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

3
许多 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会给你提供智能感知,并且编译器会在你试图访问不正确的属性时发出警告。我经常在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的博客)。
但我不禁想知道是否有办法。例如,我尝试将this T obj替换为this Base1 obj,但在这种情况下,您会失去ide对属性解析的支持,还可以编写b1.ToProp(b => b.y)。我想这可能会被运行时异常捕获。我也尝试了隐式转换 - 但不幸的是,这不是方法解析过程的一部分。
这是因为我正在扩展ReactiveUI框架以与Caliburn.Micro一起使用。 ReactiveUI有一个非常好的扩展方法ToProperty,它需要一个ReactiveUI ViewModel。通过轻微修改,我可以更改该代码以使用Caliburn.Micro视图模型。但是,然后我遇到了上述的模糊方法问题。与此同时,我只需调用Caliburn.Micro方法ToPropertyCM
有人知道我应该追求哪个聪明的途径,以使这样的事情起作用吗?并且可以扩展到新的基类类型吗?

正如您在文本中提到的那样,您是否只是想从表达式中提取属性? - poke
是的,那就是我会获取属性名的方式...只需解析表达式树(大多数实现目前具有相当有限的解析能力)。 - Gordon
2个回答

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
这个解决方案已经被 OP 抛弃了,因为你会失去 obj 的强类型。你无法在委托中调用派生类的成员。 - Thomas Levesque
@ThomasLevesque 我不确定这是否是被丢弃的解决方案:在我看来,OP不喜欢的解决方案是 ToProp<T,Y>(this Base1 obj, Func<T, Y> getter) - Sergey Kalinichenko
1
@ThomasLevesque,修改后的段落解决了这个问题,尽管根据dasb的评论,我不确定他是否真的排除了这种情况。 - Servy
@dasblinkenlight,实际上,该签名将启用对派生类成员的访问,但需要显式指定类型参数。 - Thomas Levesque
抱歉如果我表达不清楚。这是一个好主意,但它行不通。这意味着我只能获取Base1和Base2类型的属性,而不能使用派生类。我真的应该在问题中添加接口...让我修复一下。对于没有明确的问题表示歉意!需要几分钟来解决这个问题。 - Gordon
显示剩余3条评论

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

聪明,谢谢!我已经开始编写表达式代码了 - 但是在基类中实现这个想法非常好。在ExtractPropertyName中,您不需要进行类型检查以确保它适用于该对象吗? - Gordon
在我的情况下,这不是一个选项 - 我正在为现有框架添加功能 - 虽然我可以重新实现它们的基类(它们使用接口),但这是很多工作。我也不真正控制其他人的框架。但如果我完全控制,这将起作用,因为我会在两个不同的对象上实现该方法,然后根据定义为每个对象运行自定义代码。 - Gordon
ExtractPropertyName 不需要对对象进行类型检查。它要求您传递一个 Func<T> 表达式,内部的检查确保该表达式实际上是引用属性的成员表达式。每个引用属性的成员表达式都将自动工作——没有进一步限制对象的类型。 - poke
此外,该方法是静态的,因此您可以将其放入某个实用程序类中并直接调用它:Util.ExtractPropertyName(obj.Foo)。这样,您仍然可以在任何地方使用它。您还可以将其作为框架接口的扩展方法之一——甚至是 INotifyPropertyChanged 的扩展方法。 - poke

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