从反射中提取属性名称,具备智能感知和编译时检查。

15

好的。所以我有一些代码,将winForm上的某些控件映射到对象中的某些属性,以便在数据发生某些事情时对控件执行某些操作。一切都很好,也可以正常运行。不是问题所在。问题是,要添加映射项,我调用了一个类似以下函数的函数:

this.AddMapping(this.myControl,myObject,"myObjectPropertyName");

我遇到的问题是,在编译时很难区分上述行和下面这行之间的差异:

this.AddMapping(this.myControl,myObject,"myObjectPropretyName");

由于最后一个参数是一个字符串,因此没有编译时检查或任何强制执行该字符串实际上对给定对象上的有效属性名称的机制。此外,像重构和“查找所有引用”这样的操作会忽略此类引用,导致当属性名称本身更改时出现滑稽的情况。因此,我想知道是否有一种方法可以更改函数,使得我传递的仍然是某种方式表示属性名称的字符串,但具有编译时检查实际值的功能。有人说我可以使用表达式树来实现这一点,但我已经阅读了相关资料,似乎没有看到关联。我很想做这样的事情:

this.AddMapping(this.myControl,myObject,myObject.myObjectPropertyName);

或者甚至

this.AddMapping(this.myControl,myObject.myObjectPropertyName);

会很棒!

有什么想法吗?


我花了6或7次尝试才能发现你两行代码的区别。 - jjnguy
欢迎来到我的地狱...现在想象一下,它上面堆满了像CPCR、CPR、CLI等缩写词... - GWLlosa
长久以来,我一直希望有一种VS插件,它可以解析代码中的所有字符串并进行拼写检查。它还将考虑驼峰式大小写,并逐个拼写检查每个单词。有人需要编写这个东西... - BFree
1
@BFree - 不,我们需要的是缺失的 nameof(Foo)memberof(Foo) 关键字/方法。我们一直在询问和希望,但迄今为止还没有。在那之前,表达式是我所知道的最接近的。 - Marc Gravell
显示剩余3条评论
6个回答

15

在3.5中,表达式是一种将成员名称指定为代码的方法;您可以使用以下方式:

public void AddMapping<TObj,TValue>(Control myControl, TObj myObject,
       Expression<Func<TObj, TValue>> mapping) {...}

然后解析表达式树来获取值。有点低效,但不太糟糕。

以下是示例代码:

    public void AddMapping<TSource, TValue>(
        Control control,
        TSource source,
        Expression<Func<TSource, TValue>> mapping)
    {
        if (mapping.Body.NodeType != ExpressionType.MemberAccess)
        {
            throw new InvalidOperationException();
        }
        MemberExpression me = (MemberExpression)mapping.Body;
        if (me.Expression != mapping.Parameters[0])
        {
            throw new InvalidOperationException();
        }
        string name = me.Member.Name;
        // TODO: do something with "control", "source" and "name",
        // maybe also using "me.Member"
    }

调用时使用:

    AddMapping(myControl, foo, f => f.Bar);

3
为了方便使用基于表达式的 Lambda 解决方案,我将其编写为扩展方法。
  public static string GetPropertyName<T>(this object o, Expression<Func<T>> property)
    {
        var propertyInfo = (property.Body as MemberExpression).Member as PropertyInfo;
        if (propertyInfo == null)
            throw new ArgumentException("The lambda expression 'property' should point to a valid Property");
        var propertyName = propertyInfo.Name;
        return propertyName;
    }

像这样调用
    class testclass
    {
        public string s { get; set; }
        public string s2 { get; set; }
        public int i { get; set; }

    }

    [TestMethod]
    public void TestMethod2()
    {
        testclass x = new testclass();
        string nameOfPropertyS = this.GetPropertyName(() => x.s);
        Assert.AreEqual("s", nameOfPropertyS);

        string nameOfPropertyI = x.GetPropertyName(() => x.i);
        Assert.AreEqual("i", nameOfPropertyI);

    }

好的,作为扩展方法使用确实很方便,因为你实际上可以在一个类上调用另一个类的属性。我相信这种方法还有改进的空间。


我可以看到一些与扩展方法使用相关的评论,我想要尝试这个是因为在 WPF 绑定(MVVM)中,可以调用 OnPropertyChanged(this.GetProperty(() => MyProperty); 从而有助于缓解编译时安全性问题。至少如果您更改属性名称,您将获得一些编译时安全性。但是您仍然需要编辑 xaml 中的绑定文本... - Steve Adams

3

虽然这是一个老问题,但没有人更新回答以使用更近期可用的语法糖。

从C# 6开始,有一种更好的方法来获取成员名称,同时仍然引用该成员本身。它就是nameof关键字。这是一种语法糖,将替换为包含所引用的属性/成员/函数的名称的字符串。Intellisense、重构、查找所有引用都可以工作。这将适用于任何属性、字段、函数和类引用。

using System;

public class Program
{
    public static void Main()
    {
        Console.WriteLine("Name of the class is: {0}", nameof(MyClass));
        Console.WriteLine("Name of the class field is: {0}", nameof(MyClass.MyField));
        Console.WriteLine("Name of the class property is: {0}", nameof(MyClass.MyProperty));
        Console.WriteLine("Name of the class function is: {0}", nameof(MyClass.MyFunction));
    }
}

public class MyClass
{
    public int MyField;

    public int MyProperty { get; set; }

    public void MyFunction() { }
}

该程序输出以下内容:

类名为:MyClass
类字段名为:MyField
类属性名为:MyProperty
类函数名为:MyFunction

使用这个,你的代码现在变成了:

this.AddMapping(this.myControl, nameof(myObject.myObjectPropertyName));

2
考虑使用Lambda表达式或甚至是System.Linq.Expressions来实现这个,其中之一为:
extern void AddMapping<T,U>(Control control, T target, Func<T,U> mapping);
extern void AddMapping<T,U>(Control control, T target, Expression<Func<T,U>> mapping);

然后使用以下方式进行调用:
this.AddMapping(this.myControl, myObject, (x => x.PropertyName));

如果您需要在运行时拆分抽象语法树以执行反射操作(例如获取属性名称作为字符串),请使用Expression参数;或者,让委托完成提取所需数据的工作。


1

你真的不应该把字符串字面量作为属性名称传递。相反,你应该使用YourClass.PROPERTY_NAME_FOO

你应该在你的类中声明这些字符串为常量。

public const String PROPERTY_NAME_FOO = "any string, it really doesn't matter";
public const String PROPERTY_NAME_BAR = "some other name, it really doesn't matter";

或者,你根本不需要担心字符串,只需要关注属性名称:

public const int PROPERTY_NAME_FOO = 0;
public const int PROPERTY_NAME_BAR = 1; //values don't matter, as long as they are unique

这可以防止不引用有效属性的字符串进入函数调用。

智能感知将能够显示您类中的属性名称作为自动完成的建议。


我考虑过这个,唯一让我担心的是它是冗余的...现在你需要在两个不同的地方维护属性信息(常量字符串和属性名称本身),这似乎不太理想。 - GWLlosa
你真的不必使用一个字符串。你可以将它们映射到整数或其他类型。重要的是名称和值的唯一性。 - jjnguy
但是如果我不将它们映射为字符串,那么我最终如何获得一个字符串值呢? - GWLlosa
如果你需要打印某种字符串文字,那么你必须使用字符串。否则,始终使用类的属性名称来引用属性,而不是int/string值。 - jjnguy
只要你知道属性名称,就没有必要显示属性名称的字面值,因为你不需要更多的信息。 - jjnguy
显示剩余2条评论


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