如何将属性传递为委托?

18

这是一个理论问题,我已经有了解决我的问题的方案,但我认为这个问题仍然有潜在的趣味。

我能否像使用方法一样将对象属性作为委托传递?例如:

假设我有一个已加载数据的数据读取器,并且每个字段的值需要被传递到不同类型的属性中,先检查是否为 DBNull。如果尝试获取单个字段,我可能会编写类似以下代码:

if(!rdr["field1"].Equals(DBNull.Value)) myClass.Property1 = rdr["field1"];
但是如果我有100个字段,那么很快就会变得难以处理。 有几种方法可以使此调用看起来更好:
myClass.Property = GetDefaultOrValue<string>(rdr["field1"]); //Which incidentally is the route I took

这也可以作为一个扩展方法,看起来很不错:

myClass.Property = rdr["field1"].GetDefaultOrValue<string>();

或者:

SetPropertyFromDbValue<string>(myClass.Property1, rdr["field1"]); //Which is the one that I'm interested in on this theoretical level
在第二个示例中,需要将属性作为委托传递以设置它。
因此,问题分为两部分:
1.这是否可行?
2.看起来是什么样子?
[由于这只是理论上的问题,对我而言,VB或C#中的答案同样可接受]
编辑:这里有一些巧妙的答案。谢谢大家。
6个回答

17

我喜欢使用表达式树来解决这个问题。每当你想要使用“属性委托”作为方法的参数类型时,请使用Expression<Func<T, TPropertyType>>。例如:

public void SetPropertyFromDbValue<T, TProperty>(
    T obj,
    Expression<Func<T, TProperty>> expression,
    TProperty value
)
{
    MemberExpression member = (MemberExpression)expression.Body;
    PropertyInfo property = (PropertyInfo)member.Member;
    property.SetValue(obj, value, null);
}

这件好事是,对于gets,语法看起来也是一样的。

public TProperty GetPropertyFromDbValue<T, TProperty>(
    T obj,
    Expression<Func<T, TProperty>> expression
)
{
    MemberExpression member = (MemberExpression)expression.Body;
    PropertyInfo property = (PropertyInfo)member.Member;
    return (TProperty)property.GetValue(obj, null);
}

或者,如果你感到懒惰:

public TProperty GetPropertyFromDbValue<T, TProperty>(
    T obj,
    Expression<Func<T, TProperty>> expression
)
{
    return expression.Compile()(obj);
}

调用看起来像这样:

SetPropertyFromDbValue(myClass, o => o.Property1, reader["field1"]);
GetPropertyFromDbValue(myClass, o => o.Property1);

这太棒了,我正好正在思考一个类似的想法,受@JonSkeet回答的启发。我想我可以简化一下它,但是我试图在处理其他1000个日常IT问题时思考它...就像你们一样:P - BenAlabaster
我对您进行的编辑进行了+1。这段代码现在可以编译了,希望我没有出错。此外,这是一组非常好的扩展方法,尤其是public static PropertyInfo GetPropertyInfo<T, TProperty>(this Expression<Func<T, TProperty>> expression),我能够将其用作另一个库函数的基础之一,以获取Linq to Sql字符串字段的最大长度。谢谢! - Stephen Kennedy
1
@StephenKennedy,谢谢你,好发现!SO需要一个编译按钮。 ;) - Kirk Woll

12
< p >(添加第二个答案是因为它采用了完全不同的方法)< /p> < p >针对您最初的问题,即希望拥有一个良好的API将数据读取器中命名的值映射到对象属性上,请考虑 System.ComponentModel.TypeDescriptor - 这是一种经常被忽视的替代方法,以避免自己进行反射脏工作。 < p >这里有一个有用的代码片段:
var properties = TypeDescriptor.GetProperties(myObject)
    .Cast<PropertyDescriptor>()
    .ToDictionary(pr => pr.Name);

这将创建一个对象的属性描述符字典。

现在我可以这样做:

properties["Property1"].SetValue(myObject, rdr["item1"]);

PropertyDescriptorSetValue方法(与System.Reflection.PropertyInfo的等效方法不同)会为您执行类型转换-将字符串解析为整数等等。

这个方法有用的地方在于,可以想象一个基于属性的方法来迭代属性集合(PropertyDescriptor有一个Attributes属性,允许您获取添加到属性的任何自定义属性),找出在数据读取器中使用哪个值;或者有一个接收属性名-列名映射字典的方法,迭代并为您执行所有这些设置。

我认为这种方法可能会为您提供API快捷方式,以一种lambda表达式和反射技巧无法做到的方式。


我也很喜欢这个实现方式。非常好的答案。 - BenAlabaster

8
忽略是否在您特定的情况下有用(在那种情况下,我认为您所采取的方法很好),您的问题是“是否有一种将属性转换为委托的方法”。
嗯,有可能有。
每个属性实际上(在幕后)由一个或两个方法组成 - 一个设置方法和/或一个获取方法。如果您可以获得这些方法,则可以创建包装它们的委托。
例如,一旦您获得了表示类型为TObj对象的TProp属性的System.Reflection.PropertyInfo对象,我们可以创建一个Action<TObj,TProp>(即,一个接受要设置属性的对象和要设置的值的委托)来包装该setter方法,如下所示:
Delegate.CreateDelegate(typeof (Action<TObj, TProp>), propertyInfo.GetSetMethod())

或者我们可以创建一个Action<TProp>,将其包装在特定实例的TObj上,如下所示:

Delegate.CreateDelegate(typeof (Action<TProp>), instance, propertyInfo.GetSetMethod())

我们可以使用静态反射扩展方法来包装这些内容:Static Reflection
public static Action<T> GetPropertySetter<TObject, T>(this TObject instance, Expression<Func<TObject, T>> propAccessExpression)
{
    var memberExpression = propAccessExpression.Body as MemberExpression;
    if (memberExpression == null) throw new ArgumentException("Lambda must be a simple property access", "propAccessExpression");

    var accessedMember = memberExpression.Member as PropertyInfo;
    if (accessedMember == null) throw new ArgumentException("Lambda must be a simple property access", "propAccessExpression");

    var setter = accessedMember.GetSetMethod();

    return (Action<T>) Delegate.CreateDelegate(typeof(Action<T>), instance, setter);
}

现在,我可以像这样获取对象属性的“setter”委托:
MyClass myObject = new MyClass();
Action<string> setter = myObject.GetPropertySetter(o => o.Property1);

这是强类型的,基于属性本身的类型,因此在重构和编译时进行类型检查时非常健壮。

当然,在您的情况下,您希望能够使用可能为空的对象设置属性,因此强类型的包装器并不是全部解决方案 - 但它确实为您提供了可传递给SetPropertyFromDbValue方法的内容。


我相当喜欢这个。谢谢。 - BenAlabaster

6

不,对于属性而言,没有类似于方法组转换的功能。最好的办法是使用 lambda 表达式来创建一个 Func<string>(用于 getter)或 Action<string>(用于 setter):

SetPropertyFromDbValue<string>(value => myClass.Property1 = value,
                               rdr["field1"]);

1
有趣的是,我没有想到使用lambda表达式,在这种特定情况下,当然比我的第一种语句更加笨拙。不过这很有趣,我想知道是否可以进一步将其包装起来...或者可能是我的大脑正在离我远去 :P - BenAlabaster
泛型类型可以传递到委托中吗? - BenAlabaster
也许我应该把这作为另一个问题 :D - BenAlabaster

2
值得一提的是,您可以使用一些反射技巧来完成这个操作,例如...
public static void LoadFromReader<T>(this object source, SqlDataReader reader, string propertyName, string fieldName)
    {
        //Should check for nulls..
        Type t = source.GetType();
        PropertyInfo pi = t.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        object val = reader[fieldName];
        if (val == DBNull.Value)
        {
            val = default(T);
        }
        //Try to change to same type as property...
        val = Convert.ChangeType(val, pi.PropertyType);
        pi.SetValue(source, val, null);
    }

然后
myClass.LoadFromReader<string>(reader,"Property1","field1");

我曾考虑过反射,但这是我一直担心会拖慢性能的领域之一。因此,我只会在应用程序中最不可能受到影响的区域中才涉足这个领域。在加载数百个对象或解析数百个字段的数据的区域,我会担心它会对性能产生太大的影响。 - BenAlabaster
我给了你一个+1,因为它确实回答了问题,只是与我所期望的不完全一致(这个我在问题中没有明确说明)。 - BenAlabaster

0

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