许多GUI框架使用了一种非常酷的模式来确保正确编码:
interface IBase1 {}
interface IBase2 {}
class Base1 : IBase1
{
public int x { get; set; }
}
class Base2 : IBase2
{
public int y { get; set; }
}
static class Helpers
{
public static void ToProp<T,Y> (this T obj, Func<T, Y> getter)
{
}
}
class Program
{
static void Main(string[] args)
{
var b1 = new Base1();
var b2 = new Base2();
b1.ToProp(b => b.x);
b2.ToProp(b => b.y);
}
}
这里的精彩之处在于,当您键入b => b.x
时,Visual Studio将为您提供IntelliSense,如果您尝试访问不正确的属性,编译器会抱怨。我看到这在MVVM框架中经常使用。他们经常将b => b.x
作为表达式树并解析出参数的名称,以便正确引发通知属性更改消息。
我想扩展它,并用以下内容替换ToProp定义,基本上有两个代码路径,具体取决于基接口:
static class Helpers
{
public static void ToProp<T,Y> (this T obj, Func<T, Y> getter)
where T : IBase1
{
// Do something custom for 1
}
public static void ToProp<T, Y>(this T obj, Func<T, Y> getter)
where T : IBase2
{
// Do something custom for 2
}
}
这不会按原样编译 - 两个ToProp
调用都会因模糊的方法解析错误而失败。这是SO上众所周知的问题 - 对象约束不是方法解析过程的一部分(例如,参见Lipert的blog)。
但我不禁想知道是否有办法。例如,我尝试将this T obj
替换为this Base1 obj
,但在这种情况下,您放弃了ide对属性解析的支持,也可以编写b1.ToProp(b => b.y)
。我想,这可能会遇到运行时异常。我也试过了隐式转换 - 但不幸的是,这不是方法解析过程的一部分。
这是因为我正在扩展ReactiveUI框架以与Caliburn.Micro一起使用。 ReactiveUI有一个非常好的扩展方法ToProperty,它采用ReactiveUI ViewModel。通过微小的修改,我可以改变该代码以使用Caliburn.Micro视图模型。但是,我接着遇到了上面模棱两可的方法问题。与此同时,我只是调用Caliburn.Micro方法ToPropertyCM
。
任何人都知道我应该追求一个聪明的途径来做这样的工作吗?并可扩展到新的基类类型?
已编辑修复了示例以显示它是一个我感兴趣的简单界面。顺便说一下,我尝试了包装方法b.c.如果我理解方法解析,它应该允许你对模板参数的类型进行约束检查,作为解决过程的一部分。但是,正如我所提到的,隐式类型转换在解决过程中不起作用。
答案 0 :(得分:2)
只是不要使方法与该类型相关:
public static void ToProp<Y>(this Base1 obj, Func<Base1, Y> getter)
{
// Do something custom for 1
}
public static void ToProp<Y>(this Base2 obj, Func<Base2, Y> getter)
{
// Do something custom for 2
}
如果这些方法对于该类型是通用的很重要,那么您需要以某种方式更改签名以解决歧义。这样做的最有效方法是更改名称。如果针对这两种类型的行为进行了个性化,那么它们在概念上至少会执行略微不同的操作,因此您应该能够在方法的名称中反映出来。
答案 1 :(得分:1)
正如您所提到的,您实际上只对解析表达式以获取属性名称感兴趣,我将向您展示一种不同的方法,而不是关注您的代码,正如Servy的答案所显示的讨论所表明的那样,工作
所以,我自己为MVVM做了很多。我的视图模型实现INotifyPropertyChanged
并引发PropertyChanged
事件,我需要指定在事件参数中更改的属性的名称。由于这是一个字符串,因此没有固有的检查属性名称实际上是正确的。就像你说的那样,我使用lambda表达式来指定类型安全的属性(支持IntelliSense),然后解析表达式树以提取属性名称。
由于这是我只需要INotifyPropertyChanged
,我在基本视图模型中实现了实现接口和使用lambda表达式引发事件的快速方法。
所以我实际上并没有使用扩展方法。这样做的好处是,我不需要知道财产所有者的类型。例如,如果我想抛出something.Name
的事件,我不需要知道something
是什么类型。而不是运行这个:
viewModel.OnPropertyChanged(viewModel.GetPropertyNameFor(vm => vm.Name));
我只是做
viewModel.OnPropertyChanged(() => viewModel.Name);
// or actually
this.OnPropertyChanged(() => Name);
所以我们看到的表达式如下:() => obj.Property
。这是Expression<Func<T>>
,其中T
是属性的类型 - 实际上我们并不感兴趣。
提取实际上是以静态方法进行的,如下所示:
static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException("propertyExpression");
MemberExpression memberExpression = propertyExpression.Body as MemberExpression;
if (memberExpression == null)
throw new ArgumentException("The expression is not a member access expression.", "propertyExpression");
PropertyInfo property = memberExpression.Member as PropertyInfo;
if (property == null)
throw new ArgumentException("The member access expression does not access a property.", "propertyExpression");
return memberExpression.Member.Name;
}
而且这已经完成了所有工作:
var obj = new {
Foo = 123,
Bar = "baz"
};
Console.WriteLine(ExtractPropertyName(obj.Foo)); // Foo
Console.WriteLine(ExtractPropertyName(obj.Bar)); // Bar
以下所有内容只是基本视图模型中的一些辅助方法,允许调用OnPropertyChanged(Expression<Func<T>> propertyExpression)
等。
您只需更改其签名即可使该功能成为扩展方法:
static string ExtractPropertyName<T> (this INotifyPropertyChanged obj, Expression<Func<T>> propertyExpression)
{ … }
然后,您可以在任何实现INotifyPropertyChanged
的对象上调用该方法,该对象可能是您的框架。
您的示例可能如下所示:
var b1 = new Base1();
var b2 = new Base2();
// in a static Utils class
Utils.ExtractPropertyName(() => b1.x);
// or as an extension method to INPC
someViewModel.ExtractPropertyName(() => b2.y);