动态静态构造函数

本文关键字:构造函数 静态 动态 | 更新日期: 2023-09-27 18:05:17

我已经阅读了一些Stack Overflow问题和答案,以及一些博客文章(包括Jon Skeet的惰性单例初始化),它们似乎都专注于使初始化尽可能懒惰。静态初始化似乎基本上有两个选项:

  • 第一次引用类的实例或静态成员
  • 在程序开始和第一次引用之间的未指定时间。

在程序开始时,是否有办法为特定的类(或类)运行静态构造函数(或某种形式的初始化代码)?

Context:我们的库将解析传入的XML并返回对象。返回的对象类型取决于所解析的XML元素。我们提供了两个简单的类:一个是非常基本的类,允许访问属性和内部XML(作为字符串),没有任何特性;第二个是针对特定类型的对象,并为访问/编辑值提供约束检查和更多上下文特定的名称。

解析器通过查看解析器列表来确定如何解析特定的XML元素。如果它对正在解析的元素有解析器(由名称决定),则使用该解析器。如果没有,或者失败,则返回到基本解析器。

使用我们库的开发人员很可能为特定的XML元素编写自己的类。与其让他们在每个应用程序开始时手动将每个类的解析方法添加到列表中,不如让每个类都有一个静态构造函数,将自己的解析器添加到列表中,这样只需在项目中包含该类就可以注册它。但是,静态构造函数在类被实际引用之前不会触发,并且我们不能保证在解析开始之前每个这样的类都会被引用。

是否有办法保证在应用程序启动时为这些类中的每个类触发一些初始化器?这样做的好处是简单地将类包括在项目中,而不必在运行时手动将每个解析方法添加到解析器的列表中,这是一个相当小的便利,因此为了使好处值得工作,解决方案需要非常简单和直接地实现。

动态静态构造函数

是否有任何方法可以在程序开始时为特定的类(或类)运行静态构造函数(或某种形式的初始化代码)?

听起来你想要某种"模块或汇编初始化器"。我不认为这样的东西在IL中存在(尽管我可能是错的),它绝对不存在于c#中。

您总是可以创建某种属性,然后使用反射来查找用该属性装饰的所有类型,并显式初始化它们。(注意,对于泛型类型,这变得更加棘手……您可能希望将其限制为非泛型。)

编辑:我发现了更多的选项:

  • 一个构建后的工具,它创建一个初始化器,查找ModuleInitializer.Run在任何命名空间,使用Mono Cecil
  • Fody的模块初始化器插件

编辑:在更多的背景下,我怀疑任何治疗方法都比疾病本身更糟糕。任何想要编写基于反射的"查找具有此属性的所有解析器"(或类似的)的开发人员都没有太多工作要做,但我认为您不会想要干扰他们自己的应用程序启动。

为了让别人的生活更轻松,而不用强加任何东西,你可以自己包含反射部分:

public static void RegisterAllParsers(Assembly assembly)

…这可能是基于属性的。当然,它只能合理地拾取静态解析方法——如果任何开发人员有一个工厂,它可以根据工厂的初始化以不同的方式进行解析,那么您就无法轻松地自动注册它。

开发者需要调用:

LibraryClass.RegisterAllParsers(typeof(SomeTypeInProgram).Assembly);

启动。记住这一点可能并不太难——而且大多数应用程序只有一个入口点,或者至少有一些通用的启动代码。

恐怕没有办法显式地做到这一点,但您可以创建如下内容(我现在警告您,它既丑陋又不快):

[System.AttributeUsage(System.AttributeTargets.Class |
                   System.AttributeTargets.Struct)]
public class AppInitialized : System.Attribute
{
    private MethodInfo _mInfo;
    public AppInitialized(Type t, String method)
    {
        _mInfo = t.GetMethod(method, BindingFlags.Static | BindingFlags.Public);
    }
    public void Initialize()
    {
        if (_mInfo != null)
            _mInfo.Invoke(null, null);
    }
}
[AppInitialized(typeof(InitializeMe), "Initialize")]
public class InitializeMe
{
    public static void Initialize()
    {
        Console.WriteLine("InitializeMe initialized");
    }
}

然后当你的应用程序加载时,使用这样的东西来初始化所有自定义属性:

foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
{
    var a = Attribute.GetCustomAttribute(type, typeof(AppInitialized), true) 
        as AppInitialized;
    if (a != null)
        a.Initialize();
}

有点像@FlyingStreudel,我也拼凑了一些东西,"有点"做你所追求的:

属性:

[AttributeUsage(AttributeTargets.All)]
public class ModuleInitializerAttribute : Attribute
{
    private readonly string _assemblyName;
    private readonly Func<Module, bool> _modulePredicate;
    private readonly string _typeName;
    private readonly string _methodName;
    /// <summary>
    /// Only used in my test rig so I can make sure this assembly is loaded
    /// </summary>
    public static void CallMe() {}
    public ModuleInitializerAttribute(string assemblyName, string moduleName, string typeWithMethod, string methodToInvoke)
    {
        _assemblyName = assemblyName;
        _modulePredicate = mod => moduleName == null || mod.Name.Equals(moduleName, StringComparison.OrdinalIgnoreCase);
        _typeName = typeWithMethod;
        _methodName = methodToInvoke;
        AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad;
        AppDomain.CurrentDomain.DomainUnload += AppDomainUnloading;
        CheckLoadedAssemblies();
    }
    private void CheckLoadedAssemblies()
    {
        AppDomain.CurrentDomain.GetAssemblies().ToList().ForEach(this.CheckAssembly);
    }
    private void AppDomainUnloading(object sender, EventArgs e)
    {
        // Unwire ourselves
        AppDomain.CurrentDomain.AssemblyLoad -= this.OnAssemblyLoad;
        AppDomain.CurrentDomain.DomainUnload -= AppDomainUnloading;
    }
    private void OnAssemblyLoad(object sender, AssemblyLoadEventArgs args)
    {
        CheckAssembly(args.LoadedAssembly);
    }
    private void CheckAssembly(Assembly asm)
    {
        if (asm.FullName == _assemblyName)
        {
            var module = asm.GetModules().FirstOrDefault(_modulePredicate);
            if (module != null)
            {
                var type = module.GetType(string.Concat(asm.GetName().Name, ".", _typeName));
                if (type != null)
                {
                    var method = type.GetMethod(_methodName);
                    if (method != null)
                    {
                        method.Invoke(null, null);
                    }
                }
            }
        }
    }
}

试验台:

class Program
{
    [ModuleInitializer("ClassLibrary1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "ClassLibrary1.dll", "ModuleInitializerTest", "ModuleInitialize")]
    static void Main(string[] args)
    {
        Console.WriteLine("Loaded assemblies:");
        var asms = AppDomain.CurrentDomain.GetAssemblies();
        foreach (var assembly in asms)
        {
            Console.WriteLine("'tAssembly Name:{0}", assembly.GetName());
            var mods = assembly.GetModules();
            foreach (var module in mods)
            {
                Console.WriteLine("'t'tModule Name:{0}", module.Name);
            }
        }
        // This should trigger the load of the ClassLibrary1 assembly
        aReference();
        Console.ReadLine();
    }
    static void aReference()
    {
        var foo = new SomeOtherClass();         
    }
}

和另一个类lib:

namespace ClassLibrary1
{
    public class SomeOtherClass
    {
    }
    public static class ModuleInitializerTest
    {
        public static void ModuleInitialize()
        {
            // Do interesting stuff here?
        }
    }
}

我强烈建议考虑对这个(System.ComponentModel.Composition名称空间)使用托管可扩展性框架(MEF)。然后,您的客户端可以简单地添加一个[Export(typeof(ISomeParserInterface))]属性,并且MEF将能够为您的解析器提供所有可用的扩展名。

您甚至可以使用ExportMetadataAttribute来允许您的代码只实例化它所遇到的元素实际需要的解析器。

[Export(typeof(ISomeParserInterface))]
[ExportMetadata("ElementName", "SomeXmlElement")]

您可以根据XML解析的当前上下文来决定为特定的XML元素使用哪个解析器。除了根对象之外,从XML解析的每个CLR对象都将作为其成员(字段或属性)包含在其他CLR对象中。因此XML解析器可以根据成员类型(字段类型或属性类型)来确定。对于必须将XML解析为的根CLR对象,必须显式指定类型。

下面是较长的c#示例代码:

XmlParserLibrary项目:

using System;
using System.Collections.Generic;
using System.Xml;
namespace XmlParserLibrary
{
    public sealed class XmlParser
    {
        private readonly IDictionary<Type, IXmlParser> parsers = new Dictionary<Type, IXmlParser>()
        {
            { typeof(string), new StringXmlParser() }
        };
        public T Parse<T>(XmlReader reader)
        {
            return (T)this.Parse(reader, typeof(T));
        }
        public object Parse(XmlReader reader, Type type)
        {
            // Position on element.
            while (reader.Read() && reader.NodeType != XmlNodeType.Element) ;
            return GetParser(type).Parse(reader);
        }
        private IXmlParser GetParser(Type type)
        {
            IXmlParser xmlParser;
            if (!this.parsers.TryGetValue(type, out xmlParser))
                this.parsers.Add(type, xmlParser = this.CreateParser(type));
            return xmlParser;
        }
        private IXmlParser CreateParser(Type type)
        {
            var xmlParserAttribute = Attribute.GetCustomAttribute(type, typeof(XmlParserAttribute)) as XmlParserAttribute;
            return xmlParserAttribute != null ? Activator.CreateInstance(xmlParserAttribute.XmlParserType) as IXmlParser : new FallbackXmlParser(this, type);
        }
    }
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
    public sealed class XmlParserAttribute : Attribute
    {
        public Type XmlParserType { get; private set; }
        public XmlParserAttribute(Type xmlParserType)
        {
            this.XmlParserType = xmlParserType;
        }
    }
    public interface IXmlParser
    {
        object Parse(XmlReader reader);
    }
    internal sealed class StringXmlParser : IXmlParser
    {
        public object Parse(XmlReader reader)
        {
            return reader.ReadElementContentAsString();
        }
    }
    internal sealed class FallbackXmlParser : IXmlParser
    {
        private readonly XmlParser xmlParser;
        private readonly Type type;
        public FallbackXmlParser(XmlParser xmlParser, Type type)
        {
            this.xmlParser = xmlParser;
            this.type = type;
        }
        public object Parse(XmlReader reader)
        {
            var item = Activator.CreateInstance(this.type);
            while (reader.Read())
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        var propertyInfo = this.type.GetProperty(reader.LocalName);
                        var propertyValue = this.xmlParser.Parse(reader.ReadSubtree(), propertyInfo.PropertyType);
                        propertyInfo.SetValue(item, propertyValue, null);
                        break;
                }
            return item;
        }
    }
}

XmlParserLibraryTest项目:

using System.Xml;
using XmlParserLibrary;
namespace XmlParserLibraryTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var xmlParser = new XmlParser();
            Letter letter;
            using (var reader = XmlReader.Create("Letter.xml"))
                letter = xmlParser.Parse<Letter>(reader);
        }
    }
    public class Letter
    {
        public LetterAssociate Sender { get; set; }
        public LetterAssociate Receiver { get; set; }
        public LetterContent Content { get; set; }
    }
    public class LetterAssociate
    {
        public string Name { get; set; }
        public string Address { get; set; }
    }
    [XmlParser(typeof(LetterContentXmlParser))]
    public class LetterContent
    {
        public string Header { get; set; }
        public string Body { get; set; }
    }
    internal class LetterContentXmlParser : IXmlParser
    {
        public object Parse(XmlReader reader)
        {
            var content = new LetterContent();
            while (reader.Read())
                switch (reader.NodeType)
                {
                    case XmlNodeType.Element:
                        switch (reader.LocalName)
                        {
                            case "Header":
                                content.Header = reader.ReadElementContentAsString();
                                break;
                            case "Body":
                                content.Body = reader.ReadElementContentAsString();
                                break;
                        }
                        break;
                }
            return content;
        }
    }
}

Letter.xml文件:

<?xml version="1.0" encoding="utf-8" ?>
<Letter>
  <Sender>
    <Name>Sender name</Name>
    <Address>Sender address</Address>
  </Sender>
  <Receiver>
    <Name>Receiver name</Name>
    <Address>Receiver address</Address>
  </Receiver>
  <Content>
    <Header>This is letter header.</Header>
    <Body>This is letter body.</Body>
  </Content>
</Letter>

这与XmlSerializer的工作原理类似,除了它在解析时不直接使用反射,而是在解析之前在一个单独的临时程序集中生成所有解析器(在. net 4.5之前)。XmlSerializer还处理许多其他事情,例如:

  • 类层次结构(当属性类型是基类型,但实际类型序列化成XML是某种派生类型时)
  • 集合类型
  • 尊重System.Xml.Serialization命名空间中控制XML序列化/反序列化的XML序列化相关属性。

要直接回答这个问题,

是否有任何方法可以在程序开始时为特定的类(或类)运行静态构造函数(或某种形式的初始化代码)?

是的,如果你知道Type,你可以使用RuntimeHelpers.RunClassConstructor()

例如,在泛型方法

CompilerServices.RuntimeHelpers.RunClassConstructor(typeof(T).TypeHandle);

或通过Type变量

CompilerServices.RuntimeHelpers.RunClassConstructor(type.TypeHandle);

这从1.1开始就是框架的一部分,并且在。net 6中仍然存在,所以我不确定为什么没有人提到它。

作为一个例子,当使用奇怪的反复出现的模板模式作为非enum枚举类型的一部分时,我使用它来强制运行类构造函数,其中需要运行派生类的类构造函数来初始化枚举值。. net 4中添加的超惰性静态初始化导致派生类型延迟其类构造函数,甚至超过在基类上调用泛型转换操作符的时间。

通过在基类的静态构造函数中调用RunClassConstructor,您可以强制运行库也为派生类型T运行类构造函数。