.Net Lambda表达式--这个参数是从哪里来的?

17
我是一个Lambda新手,如果我在描述中遗漏了重要信息,请告诉我。我将尽可能简单的说明示例。
我正在查看其他人的代码,他们有一个类继承自另一个类。这里先给出派生类的代码以及我难以理解的Lambda表达式:
    class SampleViewModel : ViewModelBase
{
    private ICustomerStorage storage = ModelFactory<ICustomerStorage>.Create();

    public ICustomer CurrentCustomer
    {
        get { return (ICustomer)GetValue(CurrentCustomerProperty); }
        set { SetValue(CurrentCustomerProperty, value); }
    }

    private int quantitySaved;
    public int QuantitySaved
    {
        get { return quantitySaved; }
        set
        {
            if (quantitySaved != value)
            {
                quantitySaved = value;
                NotifyPropertyChanged(p => QuantitySaved); //where does 'p' come from?
            }
        }
    }

    public static readonly DependencyProperty CurrentCustomerProperty;

    static SampleViewModel()
    {
        CurrentCustomerProperty = DependencyProperty.Register("CurrentCustomer", typeof(ICustomer),
            typeof(SampleViewModel), new UIPropertyMetadata(ModelFactory<ICustomer>.Create()));
    }
//more method definitions follow..

注意上面的NotifyPropertyChanged(p => QuantitySaved)调用。我不明白这里的“p”是从哪里来的。

以下是基类:

  public abstract class ViewModelBase : DependencyObject, INotifyPropertyChanged, IXtremeMvvmViewModel
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void NotifyPropertyChanged<T>(Expression<Func<ViewModelBase, T>> property)
        {
            MvvmHelper.NotifyPropertyChanged(property, PropertyChanged);
        }
    }

这里有很多与问题无关的内容,但我想在考虑全面性方面犯错误。

问题是,我不明白'p'参数来自哪里,编译器又是如何知道(显然?)从空气中填充ViewModelBase类型值的?

为了好玩,我将代码从'p'更改为'this',因为SampleViewModel继承自ViewModelBase,但是我遇到了一系列编译器错误,其中第一个错误声明Invalid expression term '=>',这使我有点困惑,因为我认为它会起作用。

有人能解释一下这里发生了什么吗?

5个回答

17
“p”在“NotifyPropertyChanged(p => QuantitySaved);”中的lambda表达式中表示参数。将这个lambda表达式传递给一个名为“NotifyPropertyChanged”的方法。该方法有一个重载,其形式参数类型为“Expression>”。也就是说,形式参数希望得到一个lambda表达式,该表达式接受一个ViewModelBase并返回某个T。
编译器能够推断出代码作者忽略了显式说明lambda表达式参数类型的部分。“NotifyPropertyChanged((ViewModelBase p) => QuantitySaved);”也可以达到相同的效果,只不过这里明确指定了参数类型。
“编译器从哪里知道要从ViewModelBase中获取类型值?”编译器会检查所有可能接受lambda表达式的“NotifyPropertyChanged”重载方法,并从这些方法的“形式参数类型中的委托类型”中推断出lambda表达式的“形式参数类型”。举个例子:假设我们有以下代码:
void M(Func<int, double> f) {}
void M(Func<string, int> f) {}

并且调用

M(x=>x.Length);

编译器必须推断lambda参数x的类型。有哪些可能性?M有两个重载。两者都在M的正式参数中采用了一个委托,对应于调用中传递的第一个参数。在第一个函数中,函数从int到double,因此x可能是int类型。在第二个函数中,M的正式参数是从string到int的函数,因此x可能是字符串。
现在,编译器必须确定哪一个是正确的。为使第一个正确,lambda的主体必须返回double。但是如果x是int类型,则没有Length属性返回double。所以x不能是int。x可以是字符串吗?可以。如果x是字符串,则存在一个返回int的Length属性。
因此,编译器推断x是字符串。
这些推断可能会变得非常复杂。以下是稍微复杂一些的例子:
void M<A, B, C>(A a1, Func<List<A>, B> a2, Func<B, C> a3) {}
...
M(123, x=>x.Count.ToString(), y=>y.Length);

类型推断需要推断出A、B、C的类型,因此也需要推断x和y的类型。编译器首先推断A必须是int,因为a1是123。然后它推断出x必须是List<int>,基于这个事实。接着,它推断出B必须是string,因此y是string,进而导致C是y.Length的类型,即int。
从那以后,情况变得更加复杂,相信我。
如果您对这个主题感兴趣,我已经撰写了一些文章并拍摄了一些视频,介绍编译器执行各种类型推断的不同类型。请访问http://blogs.msdn.com/b/ericlippert/archive/tags/type+inference/获取所有详情。
为了好玩,我将代码从“p”改为“this”,因为SampleViewModel继承自ViewModelBase,但是我遇到了一系列编译器错误,其中第一个错误说明Invalid expression term '=>'。这有点让我困惑,因为我认为那样应该可以工作。
唯一可接受的lambda运算符左侧是lambda参数列表;"this"永远不是合法的lambda参数列表。编译器期望"this"后面跟着".SomeMethod()"或类似的东西;编译器假定"this"永远不会被"=>"跟随。当您违反该假设时,就会发生糟糕的事情。

10

p只是一个虚拟名称,它是像任何方法中的参数一样的名称。你可以将其命名为xFred,如果你喜欢的话。

请记住,Lambda表达式只是非常特殊的匿名方法。

在常规方法中,你有参数,并且它们有名称:

public double GetQuantitysaved(ViewModelBase p) {
    return QuantitySaved;
}
在匿名方法中,您有参数,并且它们都有名称:
delegate(ViewModelBase p) { return QuantitySaved; }

在 Lambda 表达式中,您有参数,它们具有名称:

p => QuantitySaved

在这三个版本中,p 扮演相同的角色。你可以随意命名它,它只是方法参数的名称。

在最后一种情况下,编译器会做很多工作来确定 p 表示类型为 ViewModelBase 的参数,以便 p => QuantitySaved 可以发挥作用。

Expression<Func<ViewModelBase, T>> property

为了好玩,我把代码中的p改成了this,因为SampleViewModel继承自ViewModelBase。但是我得到了一系列编译错误,其中第一个错误显示Invalid expression term '=>'。这让我感到有些困惑,因为我以为它可以工作。

嗯,this不是有效的参数名称,因为它是保留关键字。最好将p => QuantitySaved看作

delegate(ViewModelBase p) { return QuantitySaved; }

直到您对此概念感到舒适。在这种情况下,this 不能替换为 p,因为它不是一个有效的参数名称。


8
lambda表达式 p => QuantitySaved 是类型为 Expression<Func<ViewModelBase, int>> 的表达式。由于方法 NotifyPropertyChanged 寻找一个类型为 <ViewModelBase, T> 的表达式,所以它适用。

因此编译器能够推断出 p 是一个 ViewModelBasep 并没有“来自”任何地方,它基本上是在这里被声明的。它是 lambda 的参数。当有人使用你的方法的 property 参数时,它将被填充。例如,如果将 lambda 放入名为 lambda 的单独变量中,您可以使用 lambda(this) 调用它,并返回 QuantitySaved 值。

您无法在 lambda 中使用 this,因为它期望一个参数名称,而 this 不是有效的名称。关键是您可以在任何 ViewModelBase 实例上调用它,而不仅仅是创建 lambda 的实例。


啊,好的。所以这是一个方法声明。我以为问题中的片段正在使用'p'作为参数调用NotifyPropertyChanged()。谢谢大家,可能是我的眼睛有点累了。 - larryq
不可以使用 this,因为它是一个保留关键字。它从来都不能作为参数名。 - jason
3
@larryq:它正在使用一个名为p的参数调用NotifyPropertyChanged,该参数是一个表达式树。你不能将p与lambda分开,就像你不能将形式参数“property”的声明与NotifyPropertyChanged分开一样。 - Eric Lippert
@Tesserex:Lambda表达式的类型是Expression<Func<ViewModelBase, int>>,因为这是该方法参数的类型。 - phoog
谢谢你指出来,Eric。我应该更明确地说明传递给NotifyPropertyChanged的是表达式树,而不仅仅是'p'本身。 - larryq

4
易于理解的方法是将此替换为:
p => QuantitySaved // lambda

使用这个:

delegate (ViewModelBase p) { return QuantitySaved; } // anonymous delegate

这实际上是相同的。 p 是您匿名委托的第一个参数的参数名称。您可以为参数名称选择任何适当的名称(this 是关键字,您不能将其用作参数名称)。

在这个特定的示例中,这个 p 变量是多余的,您也可以使用无参数的委托。


在这种情况下,情况并不相同,因为lambda表达式没有转换为委托,而是转换为“Expression”。而使用“delegate”匿名函数无法实现这一点。 - svick

3

从NotifyPropertyChanged签名中:

void NotifyPropertyChanged<T>(Expression<Func<ViewModelBase, T>> property)

该方法期望的表达式接受一个类型为ViewModelBase的输入,并返回一个类型为T的实例。
参数 p 是 ViewModelBase 的一个实例。

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