使用约束重载通用扩展方法 - Func模式

时间:2014-01-09 19:49:35

标签: c# generics

许多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.如果我理解方法解析,它应该允许你对模板参数的类型进行约束检查,作为解决过程的一部分。但是,正如我所提到的,隐式类型转换在解决过程中不起作用。

2 个答案:

答案 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);