使用泛型和后期绑定的反射。如何在 运行 时间投射?

Reflection using generics and late-binding. How to cast at run-time?

我正在尝试在 C# 中使用带有反射的泛型来构建一个可以处理多个 classes 的方法。我使用具有一堆 classes 的第 3 方 DLL,在那些 classes 上,有一个我调用的方法。它们都是 return 不同的 return 类型,但是一旦我取回对象,我就会进行相同的处理(在我下面的示例中,这将是 AreaA 和 AreaB) .

基本上,我想开发一种方法,将 class 名称和预期的 return 类型作为通用变量,然后调用正确的方法 (methodName) 作为此方法的参数提供。

下面的程序编译得很好并且 运行s 没有错误,但问题是 'area' 变量的预期类型。在下面的语句中,第一行被类型转换为 (TArea),如果我将鼠标悬停在它上面 In Visual Studio,智能感知显示 属性 'name',但键入 area.name 没有给我值。我必须输入 ((AreaA)area).name

问题是类型 'AreaA' 在 运行 时可能是另一种类型。在这个例子中,'AreaB' 所以我可以硬编码一个转换。

我怎样才能将 'area' 变量转换为适当的类型,从而允许我看到第 3 方 class 的 public methods/properties?

注意:在我的示例中,所有内容都在同一个 class 中,但实际上 ServiceA、ServiceB、AreaA 和 AreaB 的定义将在第 3 方 DLL 中。

一如既往,提前致谢!

图 1 - 如果转换为 'AreaA','area' 变量只能得到 'name' 属性

area = (TArea)dfpMethod.Invoke(instance, new object[] { "Area123" });
AreaA areaa = (AreaA)dfpMethod.Invoke(instance, new object[] { "Area123" });

图 2. - 完整程序

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;
using System.IO;


namespace ReflectionExample
{
    class Sample
    {
        class ServiceA
        {
            public int size {get; set;}
            public string name {get; set;}

            public ServiceA()
            {
                name = "TestA";
                size = 100;
            }
            public AreaA doWork(string name)
            {
                return new AreaA(name);

            }
        }   

        class AreaA
        {
            public string name { get; set;}
            public AreaA(string name)
            {
                this.name = name;
            }
            public AreaA()
            {

            }

         }

        class ServiceB
        {
            public int size { get; set; }
            public string name { get; set; }

            public ServiceB()
            {
                name = "TestB";
                size = 50;
            }
            public AreaB doWork(string name)
            {
                return new AreaB(name);
            }

        }

        class AreaB
        {
            public string name { get; set; }
            public AreaB(string name)
            {
                this.name = name;
            }
            public AreaB()
            {

            }
        }

        static void Main(string[] args)
        {
            runService<ServiceA, AreaA>("doWork");
        }

        private static void runService<TService, TArea>(string methodName)
            where TService : class, new()
            where TArea : class, new()
        {
            //Compile time processing
            Type areaType = typeof(TArea);  
            Type serviceType = typeof(TService);


            //Print the full assembly name and qualified assembly name
            Console.WriteLine("AreaType--Full assembly name:\t   {0}.", areaType.Assembly.FullName.ToString());            // Print the full assembly name.
            Console.WriteLine("AreaType--Qualified assembly name:\t   {0}.", areaType.AssemblyQualifiedName.ToString());   // Print the qualified assembly name.
            Console.WriteLine("ServiceType--Full assembly name:\t   {0}.", serviceType.Assembly.FullName.ToString());            // Print the full assembly name.
            Console.WriteLine("ServiceType--Qualified assembly name:\t   {0}.", serviceType.AssemblyQualifiedName.ToString());   // Print the qualified assembly name.

            //This is done because in my code, the assembly doesn't reside in the executiy assembly, it is only setup as a reference
            var assembly = Assembly.Load(serviceType.Assembly.FullName);        

            //Initialize the generic area
            TArea area = default(TArea);
            //Get an instance of the service so I can invoke the method later on
            var instance = Activator.CreateInstance(serviceType);

            //Get the methodInfo for the methodName supplied to the runService method
            MethodInfo dfpMethod = serviceType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance);

            //area is type casted to (TArea), the intellisense shows the property 'name', but typing area.name doesn't give me the value
            //I have to type ((AreaA)area).name.  Problem is the type 'AreaA' could be another type.  In this example, 'AreaB'
            area = (TArea)dfpMethod.Invoke(instance, new object[] { "Area123" });
            AreaA areaa = (AreaA)dfpMethod.Invoke(instance, new object[] { "Area123" });
            Console.WriteLine();

        }

    }
}

我认为您在这里遇到的问题是将动态加载的类型 (Assembly.Load()) 与直接从您的项目引用的类型混合在一起,您可以在智能感知中看到,即 AreaA。

如果您使用反射动态加载整个程序集,intellisense 将不会帮助您查看 class 成员,因为该信息需要在编译时知道,并且根据定义,您是在加载程序集运行时间。

如果您只想查看您的类型可用的所有 public 属性的列表,那么您可以使用此:

var areaProperties = area.GetType().GetProperties();

但同样,这都是在 运行 时间完成的,因此在编写代码时对您没有帮助。

您可以使用以下方法动态读取 "name" 属性 的值:

var nameValue = area.GetType().GetProperty("name").GetValue(area);

本质上,如果您需要智能感知,请直接从您的 Visual Studio 项目中引用 dll,而不是使用 Assembly.Load()。 希望对您有所帮助。

您的错误来源是您将所有 return 值强制转换为类型 TArea 并使用以下语句:

TArea area = (TArea)dfpMethod.Invoke(instance, new object[] { "Area123" });

根据您的通用规范,TArea 类型唯一向您承诺的是它是一个 class。因此,除了 'object' 类型的成员之外,TArea 不允许您访问任何内容。

相反,取消 TArea 通用参数,转而使用 'dynamic' 关键字:

var area = (dynamic)dfpMethod.Invoke(instance, new object[] { "Area123" });
return area.name; // no error

请注意,只有在第三方库中定义了实际类型 AreaA 和 AreaB(如您所说)并且您无法修改它们时,这才相关。如果你不能修改 classes,你就不能引入接口(这是你真正需要的)。如果不能引入接口,但所有类型都公开相同的签名,则可以使用动态类型假设相关成员的存在。

如果您需要使用 AreaA/AreaB 做很多工作并且您不希望所有动态操作的性能开销,请定义您自己的通用 class 公开您的所有签名需要:

public class MyGenericArea 
{
    public MyGenericArea(string name)
    {
         this.Name = name;
    }
    public string Name {get; set;}
} 

然后使用 class 类型的动态转换和 return 填充 class:

 var area = (dynamic)dfpMethod.Invoke(instance, new object[] { "Area123" });
 return new MyGenericArea(area.name);