如果 vs 重载 vs 反射

3

我有很多带有属性的类,例如:

class C1
{
    [PropName("Prop1")]
    public string A {get;set;}

    [PropName("Prop2")]
    public string B {get;set;}

    [PropName("Prop3")]
    public string C {get;set;}
} 

class C2
{
    [PropName("Prop1")]
    public string D {get;set;}

    [PropName("Prop2")]
    public string E {get;set;}

    [PropName("Prop3")]
    public string F {get;set;}
} 

该属性告诉实际的属性是什么,但C#属性的名称并不总是匹配的。在C1和C2的情况下,C1.A与C2.D是相同的属性。

这些类不属于任何继承链,并且我无法控制它们,因此无法更改它们。

"Prop1",“Prop2”等具有一些常见操作。最好的解决方案是什么,可以在不重复过多代码的情况下编写这些操作,同时仍然使其可维护。

解决方案#1(如果语句-很多)

void OperationWithProp1(object o)
{
    string prop1;        

    C1 class1 = o as C1;
    if (class1 != null)
        prop1 = class1.A;

    C2 class2 = o as C2;
    if (class2 != null)
        prop1 = class2.D;

    // Do something with prop1
}

解决方案 #2(重载 - 大量使用)
void OperationWithProp1(string prop1)
{
    // Do something with prop1
}

void RunOperationWithProp1(C1 class1)
{
    OperationWithProp1(class1.A);
}

void RunOperationWithProp1(C2 class2)
{
    OperationWithProp1(class2.D);
}

解决方案 #3(反射)- 我担心性能问题,因为这些操作将被调用数千次,而且有几百个操作。
void OperationWithProp1(object o)
{
     // Pseudo code:
     // Get all properties from o that have the PropName attribute
     // Look if any attribute matches "Prop1"
     // Get the value of the property that matches
     // Do something with the value of the property
}

您会选择哪个解决方案并为什么?您有其他模式想法吗?

为澄清而进行的编辑:

很多类意味着有数十个类

很多属性意味着每个类有30-40个属性


乍一看,我可能会选择#2,仅仅是为了在编译时保证安全性并避免过多的类型转换。这可能取决于"lots of them" 实际上指的是什么:你能提供一些关于使用规模的实际感觉吗?我们是在讨论几十个、数十个还是上百个的数量级?编辑:另一个选择是提供自己的对象包装器来处理这个问题,并隐藏对外部API/业务逻辑的复杂性。 - Chris Sinclair
如果您不希望编写大量的样板代码,更好的方法是使用反射,您可以缓存一些东西,例如PropertyInfo实例,以加快速度。 - Federico Berasategui
1
我相信反射无论你使用一次还是多次,都会产生相同的速度影响 - 只是第一次调用时需要一点时间来检查所有内容,或者类似于这样的事情。(我不是完全确定。) - Bobson
@Bobson 真的取决于你如何利用它;我想反射 API 在幕后可能会进行一些缓存,但与完全使用反射相比,收益可能会很小。(一些包装反射的库将在运行时执行自己的缓存或编写 IL 以使其快速,因此您可以利用这些而不是直接使用反射。我认为 dynamic 对象也有一些缓存)编辑:也许我误解了你。 :) 我今天思维混乱! - Chris Sinclair
4个回答

4
您可以创建一个封装类,暴露需要的属性,并对实际的 C1 和 C2 类的实例进行包装。一种方法是通过委托来实现:
interface WithProperties {
   string A {get;set;}
   string B {get;set;}
   string C {get;set;}
}
class WrappedCX<T> : WithProperties {
    private readonly T wrapped;
    private readonly Func<T,string> getA;
    private readonly Action<T,string> setA;
    private readonly Func<T,string> getB;
    private readonly Action<T,string> setB;
    private readonly Func<T,string> getC;
    private readonly Action<T,string> setC;
    public WrappedCX(T obj, Func<T,string> getA, Action<T,string> setA, Func<T,string> getB, Action<T,string> setB, Func<T,string> getC, Action<T,string> setC) {
        wrapped = obj;
        this.getA = getA;
        this.setA = setA;
        this.getB = getB;
        this.setB = setB;
        this.getC = getC;
        this.setC = setC;
    }
    public string A {
        get {return getA(wrapped);}
        set {setA(wrapped, value);}
    }
    public string B {
        get {return getB(wrapped);}
        set {setB(wrapped, value);}
    }
    public string C {
        get {return getC(wrapped);}
        set {setC(wrapped, value);}
    }
}

现在你可以像这样操作:

C1 c1 = new C1();
C2 c2 = new C2();
WithProperties w1 = new WrappedCX(c1, c => c.A, (c,v) => {c.A=v;}, c => c.B, (c,v) => {c.B=v;}, c => c.C, (c,v) => {c.C=v;});
WithProperties w2 = new WrappedCX(c2, c => c.D, (c,v) => {c.D=v;}, c => c.E, (c,v) => {c.E=v;}, c => c.F, (c,v) => {c.F=v;});

此时,w1w2都实现了通用的WithProperties接口,因此您可以在不检查其类型的情况下使用它们。

要更高级,可以将七个参数的构造函数替换为一个接受单个obj参数的构造函数,通过反射获取其类,检查其属性以查找您定义的自定义属性,并创建/编译与属性ABC的getter和setter相对应的LINQ表达式。这将使您构造WrappedCX时不再需要在调用中使用丑陋的lambda。但这样做的权衡是,现在lambda将在运行时构造,因此缺少属性的编译错误将变成运行时异常。


我没有尝试编译这个程序,所以里面可能会有一些小的语法错误。不过总体策略应该是正确的。如果你发现代码中有错误,请告诉我,我会编辑答案来修复它们。 - Sergey Kalinichenko
我认为,如果首先定义一个转换器接口 IAccessor<T>,该接口将存储或获取适当的属性到或从 T 中,则这种方法可能会更好地工作。如果这样做了,有一个静态类 Accessors<T>,其中有一个类型为 IAccessor<T> 的单个字段 Accessor,并且对于每个适当的类型,将 Accessors<TT>.Accessor 设置为实现了 IAccessor<TT> 的内容,那么对于任何安装了转换器的泛型类型 TTT 的变量 it,可以使用类似 Accessors<TTT>.Accessor.SetProperty1(it, "Fred"); 的方式将 property1 设置为 "Fred"。 - supercat
@supercat 对的 - 这可能是从答案中实现“变得花哨”的一种方式。 - Sergey Kalinichenko
我猜问题在于是否希望要求在构建基础对象同时构建包装器,并要求所有传递该对象的代码必须传递包装器,还是希望允许代码传递原始对象类型并保留访问其属性的能力。如果某些代码需要使用其基础类型,那么包装器方法往往会违反我的面向对象编程规则之一:任何一个实体都应将系统状态的任何可变方面视为其自身状态的一部分。 - supercat

3
你可以动态生成代理类,使用属性“PropName”名称访问正确的成员。在生成调用之前,还需要检测属性是否实际实现了get/set。此外,可能需要更复杂的方法来保证生成的代理的唯一类型名称...
请查看Main()以获取用法,下面的代码是OperationWithProp1()的实现。
public interface IC
{
    string Prop1 { get; set; }
    string Prop2 { get; set; }
    string Prop3 { get; set; }
}

public class C1
{
    [PropName("Prop1")]
    public string A { get; set; }

    [PropName("Prop2")]
    public string B { get; set; }

    [PropName("Prop3")]
    public string C { get; set; }
}

public class C2
{
    [PropName("Prop1")]
    public string D { get; set; }

    [PropName("Prop2")]
    public string E { get; set; }

    [PropName("Prop3")]
    public string F { get; set; }
}

public class ProxyBuilder
{
    private static readonly Dictionary<Tuple<Type, Type>, Type> _proxyClasses = new Dictionary<Tuple<Type, Type>, Type>();

    private static readonly AssemblyName _assemblyName = new AssemblyName("ProxyBuilderClasses");
    private static readonly AssemblyBuilder _assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(_assemblyName, AssemblyBuilderAccess.RunAndSave);
    private static readonly ModuleBuilder _moduleBuilder = _assemblyBuilder.DefineDynamicModule(_assemblyName.Name, _assemblyName.Name + ".dll");

    public static void SaveProxyAssembly()
    {
        _assemblyBuilder.Save(_assemblyName.Name + ".dll");
    }

    public static Type GetProxyTypeForBackingType(Type proxyInterface, Type backingType)
    {
        var key = Tuple.Create(proxyInterface, backingType);

        Type returnType;
        if (_proxyClasses.TryGetValue(key, out returnType))
            return returnType;

        var typeBuilder = _moduleBuilder.DefineType(
            "ProxyClassProxies." + "Proxy_" + proxyInterface.Name + "_To_" + backingType.Name,
            TypeAttributes.Public | TypeAttributes.Sealed,
            typeof (Object),
            new[]
            {
                proxyInterface
            });

        //build backing object field
        var backingObjectField = typeBuilder.DefineField("_backingObject", backingType, FieldAttributes.Private);

        //build constructor
        var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] {backingType});
        var ctorIL = ctor.GetILGenerator();
        ctorIL.Emit(OpCodes.Ldarg_0);
        var ctorInfo = typeof (Object).GetConstructor(types: Type.EmptyTypes);
        ctorIL.Emit(OpCodes.Call, ctorInfo);
        ctorIL.Emit(OpCodes.Ldarg_0);
        ctorIL.Emit(OpCodes.Ldarg_1);
        ctorIL.Emit(OpCodes.Stfld, backingObjectField);
        ctorIL.Emit(OpCodes.Ret);

        foreach (var targetPropertyInfo in backingType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
        {
            var propertyName = targetPropertyInfo.Name;
            var attributes = targetPropertyInfo.GetCustomAttributes(typeof (PropName), true);

            if (attributes.Length > 0 && attributes[0] != null)
                propertyName = (attributes[0] as PropName).Name;

            var propBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, targetPropertyInfo.PropertyType, null);

            const MethodAttributes getSetAttrs =
                MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig | MethodAttributes.Final | MethodAttributes.Virtual;

            //build get method
            var getBuilder = typeBuilder.DefineMethod(
                "get_" + propertyName,
                getSetAttrs,
                targetPropertyInfo.PropertyType,
                Type.EmptyTypes);

            var getIL = getBuilder.GetILGenerator();
            getIL.Emit(OpCodes.Ldarg_0);
            getIL.Emit(OpCodes.Ldfld, backingObjectField);
            getIL.EmitCall(OpCodes.Callvirt, targetPropertyInfo.GetGetMethod(), Type.EmptyTypes);
            getIL.Emit(OpCodes.Ret);
            propBuilder.SetGetMethod(getBuilder);

            //build set method
            var setBuilder = typeBuilder.DefineMethod(
                "set_" + propertyName,
                getSetAttrs,
                null,
                new[] {targetPropertyInfo.PropertyType});

            var setIL = setBuilder.GetILGenerator();
            setIL.Emit(OpCodes.Ldarg_0);
            setIL.Emit(OpCodes.Ldfld, backingObjectField);
            setIL.Emit(OpCodes.Ldarg_1);
            setIL.EmitCall(OpCodes.Callvirt, targetPropertyInfo.GetSetMethod(), new[] {targetPropertyInfo.PropertyType});
            setIL.Emit(OpCodes.Ret);
            propBuilder.SetSetMethod(setBuilder);
        }
        returnType = typeBuilder.CreateType();
        _proxyClasses.Add(key, returnType);
        return returnType;
    }

    public static TIProxy CreateProxyObject<TIProxy>(object backingObject, out TIProxy outProxy) where TIProxy : class
    {
        var t = GetProxyTypeForBackingType(typeof (TIProxy), backingObject.GetType());
        outProxy = Activator.CreateInstance(t, backingObject) as TIProxy;
        return outProxy;
    }


    private static void Main(string[] args)
    {
        var c1 = new C1();
        IC c1Proxy;
        CreateProxyObject(c1, out c1Proxy);
        var c2 = new C2();
        IC c2Proxy;
        CreateProxyObject(c2, out c2Proxy);

        c1Proxy.Prop1 = "c1Prop1Value";
        Debug.Assert(c1.A.Equals("c1Prop1Value"));

        c2Proxy.Prop1 = "c2Prop1Value";
        Debug.Assert(c2.D.Equals("c2Prop1Value"));

        //so you can check it out in reflector
        SaveProxyAssembly();
    }

    private static void OperationWithProp1(object o)
    {
        IC proxy;
        CreateProxyObject(o, out proxy);

        string prop1 = proxy.Prop1;

        // Do something with prop1
    }

1
为了获得最佳性能,您应该为每个属性编写一对静态方法,格式如下:
[PropName("Prop1")]
static string Prop1Getter(thisType it) { return it.WhateverProperty; }
[PropName("Prop1")]
static string Prop1Setter(thisType it, string st) { it.WhateverProperty = st; }

我建议您使用反射来生成委托,并使用静态泛型类来缓存它们。实际上,您将拥有一个名为PropertyAccessors<T>的私有静态类,其中声明了像这样的委托:
const int numProperties = 3;
public Func<T, string>[] Getters;
public Action<T, string>[] Setters;

静态构造函数将会执行类似以下的操作:
Getters = new Func<T, string>[numProperties];
Setters = new Action<T, string>[numProperties];
for (int i = 0; i< numProperties; i++)
{
  int ii = i;  // Important--ensure closure is inside loop
  Getters[ii] = (T it) => FindSetAndRunGetter(ii, it);
  Setters[ii] = (T it, string st) => FindSetAndRunSetter(ii, it, st);
}

FindSetAndRunGetter(ii,it) 方法应该搜索适当的属性 getter,如果找到,则将 Getters[ii] 设置为指向适当的属性 getter,运行一次并返回结果。 FindSetAndRunSetter(ii, it, st) 应该使用属性 setter 进行类似操作,使用 st 作为参数运行一次。

使用这种方法将结合使用反射的灵活性和“自动升级”(即自动查找未来类中的方法)以及与硬编码方法相当甚至更好的速度。唯一的烦恼是需要按照上述描述定义静态方法。可能可以使用 Reflection.Emit 自动生成包含此类方法的静态类,但这超出了我的专业水平。


1

我认为,为了清晰度和可维护性而选择过载。如果存在很多重叠的代码,请将其拆分成单独的方法。

话虽如此,我假设您首先关心的是可维护性,因为您没有提到速度。


1
你会选择哪个解决方案,以及为什么? - antonijn
我不喜欢在这种情况下使用重载,因为它们与泛型的交互效果不佳。从概念上讲,应该能够编写一个方法,该方法可以接受泛型类型T的参数,并访问任何具有适当属性的传入对象的property1,但是C#只能将类型为“T”的参数传递给可以接受所有类型“T”可能的重载。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接