【问题标题】:.NET assemblies: understanding type visibility.NET 程序集:了解类型可见性
【发布时间】:2010-08-07 22:27:16
【问题描述】:

我正在尝试重现 System.Xml.Serialization 已经做过的事情,但是用于不同的数据源。 目前任务仅限于反序列化。 IE。给定我知道如何阅读的已定义数据源。编写一个采用随机类型的库,通过反射了解它的字段/属性,然后生成并编译可以获取数据源和该随机类型的实例的“读取器”类,并从数据源写入对象的字段/属性。

这是我的 ReflectionHelper 类的简化摘录

public class ReflectionHelper
{
    public abstract class FieldReader<T> 
    {
        public abstract void Fill(T entity, XDataReader reader);
    }

    public static FieldReader<T> GetFieldReader<T>()
    {
        Type t = typeof(T);
        string className = GetCSharpName(t);
        string readerClassName = Regex.Replace(className, @"\W+", "_") + "_FieldReader";
        string source = GetFieldReaderCode(t.Namespace, className, readerClassName, fields);

        CompilerParameters prms = new CompilerParameters();
        prms.GenerateInMemory = true;
        prms.ReferencedAssemblies.Add("System.Data.dll");
        prms.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().GetModules(false)[0].FullyQualifiedName);
        prms.ReferencedAssemblies.Add(t.Module.FullyQualifiedName);

        CompilerResults compiled = new CSharpCodeProvider().CompileAssemblyFromSource(prms, new string[] {source});

        if (compiled.Errors.Count > 0)
        {
            StringWriter w = new StringWriter();
            w.WriteLine("Error(s) compiling {0}:", readerClassName);
            foreach (CompilerError e in compiled.Errors)
                w.WriteLine("{0}: {1}", e.Line, e.ErrorText);
            w.WriteLine();
            w.WriteLine("Generated code:");
            w.WriteLine(source);
            throw new Exception(w.GetStringBuilder().ToString());
        }

        return (FieldReader<T>)compiled.CompiledAssembly.CreateInstance(readerClassName);
    }

    private static string GetFieldReaderCode(string ns, string className, string readerClassName, IEnumerable<EntityField> fields)
    {
        StringWriter w = new StringWriter();

        // write out field setters here

        return @"
using System;
using System.Data;

namespace " + ns + @".Generated
{
    public class " + readerClassName + @" : ReflectionHelper.FieldReader<" + className + @">
    {
        public void Fill(" + className + @" e, XDataReader reader)
        {
" + w.GetStringBuilder().ToString() + @"
        }
    }
}
";
    }
}

和调用代码:

class Program
{
    static void Main(string[] args)
    {
        ReflectionHelper.GetFieldReader<Foo>();
        Console.ReadKey(true);
    }

    private class Foo
    {
        public string Field1 = null;
        public int? Field2 = null;
    }
}

动态编译当然会失败,因为 Foo 类在 Program 类之外是不可见的。但! .NET XML 反序列化程序以某种方式解决了这个问题——问题是:如何? 通过反射器挖掘 System.Xml.Serialization 一个小时后,我开始接受我在这里缺乏一些基本知识并且不确定我在寻找什么......

另外,我完全有可能在重新发明轮子和/或朝错误的方向挖掘,在这种情况下,请大声说出来!

【问题讨论】:

  • 您遇到了哪些错误,报告在哪里?
  • CompileAssemblyFromSource() 返回稍后由throw new Exception(w.GetStringBuilder().ToString()); 抛出的错误。我回家后会检查确切的消息,但基本上归结为“Foo 类不可见”

标签: c# .net reflection assemblies


【解决方案1】:

您无需创建动态程序集并动态编译代码即可反序列化对象。 XmlSerializer 也不这样做——它使用反射 API,特别是它使用以下简单概念:

检索任意类型的字段集

Reflection 为此提供了GetFields() 方法:

foreach (var field in myType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
    // ...

我在此处包含BindingFlags 参数以确保它将包含非公共字段,否则默认情况下它将仅返回公共字段。

设置任意类型的字段值

Reflection 为此提供了函数SetValue()。您在 FieldInfo 实例(从上面的 GetFields() 返回)上调用它,并为其提供您要更改该字段值的实例,以及将其设置为的值:

field.SetValue(myObject, myValue);

这基本上等同于myObject.Field = myValue;,当然除了该字段是在运行时而不是编译时识别的。

把它们放在一起

这是一个简单的例子。请注意,您需要进一步扩展它以处理更复杂的类型,例如数组。

public static T Deserialize<T>(XDataReader dataReader) where T : new()
{
    return (T) deserialize(typeof(T), dataReader);
}
private static object deserialize(Type t, XDataReader dataReader)
{
    // Handle the basic, built-in types
    if (t == typeof(string))
        return dataReader.ReadString();
    // etc. for int and all the basic types

    // Looks like the type t is not built-in, so assume it’s a class.
    // Create an instance of the class
    object result = Activator.CreateInstance(t);

    // Iterate through the fields and recursively deserialize each
    foreach (var field in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
        field.SetValue(result, deserialize(field.FieldType, dataReader));

    return result;
}

请注意,我必须对XDataReader 做出一些假设,最值得注意的是它只能读取这样的字符串。我相信您可以对其进行更改,使其适用于您的特定阅读器类。

一旦你扩展它以支持你需要的所有类型(包括你的示例类中的int?),你可以通过调用反序列化一个对象:

Foo myFoo = Deserialize<Foo>(myDataReader);

即使Foo 是私有类型(如您的示例中所示),您也可以这样做。

【讨论】:

  • 不,它使用反射来获取有关类型成员的信息并生成反序列化器类。这正是我的GetFieldReaderCode() 的最后一个参数是如何获得的。它不使用反射来分配成员值的主要原因是因为反射很慢。以这种方式反序列化 1k 对象将是一个严重的拖累(尤其是具有很多成员的类型)
  • 有趣。我不知道。但是,生成该动态程序集的速度非常慢。我自己的基于反射的 XML 序列化器对于单个对象大约快 10 倍; XmlSerializer 仅在您要反序列化同一类型超过 25 次时才值得。 (在总共 335 个字段的对象图上进行测试,XML 输出约 20 KB。)
  • 是的,这就是预期。生成、编译和加载程序集的初始投资显然是有代价的,但这是可以接受的。还预计我的示例代码可能不是最理想的 - 它只是一个概念证明。
【解决方案2】:

如果我尝试使用 sgen.exe(独立的 XML 序列化程序集编译器),我会收到以下错误消息:

Warning: Ignoring 'TestApp.Program'.
  - TestApp.Program is inaccessible due to its protection level. Only public types can be processed.
Warning: Ignoring 'TestApp.Program+Foo'.
  - TestApp.Program+Foo is inaccessible due to its protection level. Only public types can be processed.
Assembly 'c:\...\TestApp\bin\debug\TestApp.exe' does not contain any types that can be serialized using XmlSerializer.

在您的示例代码中调用 new XmlSerializer(typeof(Foo)) 会导致:

System.InvalidOperationException: TestApp.Program+Foo is inaccessible due to its protection level. Only public types can be processed.

那么是什么让您想到 XmlSerializer 可以处理这个问题?

但是,请记住,在运行时,没有这样的限制。使用反射的可信代码可以随意忽略访问修饰符。这就是 .NET 二进制序列化正在做的事情。

例如,如果您在运行时使用 DynamicMethod 生成 IL 代码,那么您可以传递 skipVisibility = true 以避免对字段/类的可见性进行任何检查。

【讨论】:

  • 你说得对......我不敢相信我认为它做到了......我想我可以忍受这个限制。在这里,有一些代表 :) 编辑:嗯,它不会让我奖励赏金 - 告诉我等待 6 小时
  • 在决定将赏金授予谁之前,请考虑查看其他答案。
  • 没有其他答案仅仅是因为这个问题是基于错误的假设提出的。丹尼尔是 110% 正确的。在尝试解决问题(而我没能正确地做到这一点)之前,他确实做了一个优秀的开发人员应该做的事情:验证问题的证据。在我看来,他绝对应得的。
【解决方案3】:

我在这方面做了一些工作。我不确定它是否会有所帮助,但无论如何我认为这可能是一种方式。最近,我处理了一个我必须通过网络发送的类的序列化和反序列化。由于有两个不同的程序(客户端和服务器),起初我在两个源中都实现了该类,然后使用了序列化。它失败了,因为 .Net 告诉我它的 ID 不同(我不确定,但它是某种程序集 ID)。

好吧,经过一番谷歌搜索后,我发现这是因为序列化的类位于不同的程序集上,所以解决方案是将该类放在一个独立的库中,然后使用该库编译客户端和服务器。我在您的代码中使用了相同的想法,因此我将 Foo 类和 FieldReader 类都放在了一个独立的库中,假设:

namespace FooLibrary
{    
    public class Foo
    {
        public string Field1 = null;
        public int? Field2 = null;
    }

    public abstract class FieldReader<T>
    {
        public abstract void Fill(T entity, IDataReader reader);
    }    
}

编译它并将其添加到其他源 (using FooLibrary;)

这是我使用的代码。它与您的不完全相同,因为我没有 GetCSharpName(我使用 t.Name 代替)和 XDataReader 的代码,所以我使用了 IDataReader(只是为了让编译器接受代码并编译它)并更改 EntityField对象

public class ReflectionHelper
{
    public static FieldReader<T> GetFieldReader<T>()
    {
        Type t = typeof(T);
        string className = t.Name;
        string readerClassName = Regex.Replace(className, @"\W+", "_") + "_FieldReader";
        object[] fields = new object[10];
        string source = GetFieldReaderCode(t.Namespace, className, readerClassName, fields);

        CompilerParameters prms = new CompilerParameters();
        prms.GenerateInMemory = true;
        prms.ReferencedAssemblies.Add("System.Data.dll");
        prms.ReferencedAssemblies.Add(Assembly.GetExecutingAssembly().GetModules(false)[0].FullyQualifiedName);
        prms.ReferencedAssemblies.Add(t.Module.FullyQualifiedName);
        prms.ReferencedAssemblies.Add("FooLibrary1.dll");

        CompilerResults compiled = new CSharpCodeProvider().CompileAssemblyFromSource(prms, new string[] { source });

        if (compiled.Errors.Count > 0)
        {
            StringWriter w = new StringWriter();
            w.WriteLine("Error(s) compiling {0}:", readerClassName);
            foreach (CompilerError e in compiled.Errors)
                w.WriteLine("{0}: {1}", e.Line, e.ErrorText);
            w.WriteLine();
            w.WriteLine("Generated code:");
            w.WriteLine(source);
            throw new Exception(w.GetStringBuilder().ToString());
        }

        return (FieldReader<T>)compiled.CompiledAssembly.CreateInstance(readerClassName);
    }

    private static string GetFieldReaderCode(string ns, string className, string readerClassName, IEnumerable<object> fields)
    {
        StringWriter w = new StringWriter();

        // write out field setters here

        return @"   
using System;   
using System.Data;   
namespace " + ns + ".Generated   
{    
   public class " + readerClassName + @" : FieldReader<" + className + @">    
   {        
         public override void Fill(" + className + @" e, IDataReader reader)          
         " + w.GetStringBuilder().ToString() +         
   }    
  }";        
 } 
}

顺便说一下,我发现了一个小错误,你应该使用 new 或 override 与 Fill 方法,因为它是抽象的。

好吧,我必须承认 GetFieldReader 返回 null,但至少编译器会编译它。

希望这会对您有所帮助,或者至少可以引导您找到正确的答案 问候

【讨论】:

  • Foo 类不需要在单独的程序集中,它只需要公开,我的原始代码就可以工作。同样的问题也适用于您的代码 - 将 Foo 更改为 private 会中断(它必须嵌套在其他一些类中)。我的示例中解决方案的布局正是它需要的样子,否则一开始就没有动态编译的意义。是的,我知道我的示例中的GetFieldReader&lt;T&gt;() 返回null - 那是因为它将readerClassName 传递给没有命名空间的CreateInstance()...我从那时起修复了它:)
猜你喜欢
  • 2011-07-05
  • 2015-04-06
  • 1970-01-01
  • 2012-04-23
  • 2021-12-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多