【问题标题】:Expressions definition clarification表达式定义说明
【发布时间】:2013-11-11 02:55:51
【问题描述】:

谁能对这里发生的事情提供一个清晰(且易于理解)的解释(一般来说,泛型、扩展方法和表达式):

    public static MvcHtmlString TextBoxFor<TModel, TProperty>
        (this HtmlHelper<TModel> htmlHelper, 
              Expression<Func<TModel, TProperty>> expression)
    {
        return htmlHelper.TextBoxFor(expression, format: null);
    }

在这里进一步使用它:

    Html.TextBoxFor(o => Model.SomeValue)

最难理解的时刻是Expression 参数在这种情况下的工作方式。我知道 Generics 是如何工作的(通常是他),我也知道这是一个 扩展方法(以及它们也是如何工作的),但 无法理解如何然后在 Html.TextBoxFor 方法中处理(或处理等)表达式(相对于 TModel 和 Func 的 TProperty)。

提供的代码来自 ASP.NET MVC,但这根本与 MVC 无关:问题仅与表达式相关

谢谢!

编辑:

经过一番调查,剩下的问题主要是:TProperty类参数在提供的方法定义中的作用是什么,它是如何影响方法调用的?

【问题讨论】:

    标签: c# asp.net-mvc generics expression extension-methods


    【解决方案1】:

    首先你应该明白Expression&lt;Func&lt;...&gt;&gt;Func&lt;...&gt; 本质上是同一个东西。事实上,C# 编译器可以将任何 Func 文字隐式转换为等效的 Expression

    Func&lt;TModel, TProperty 表示“一个接受 TModel 实例并返回 TProperty 值的委托(函数)”。抛开整个表达式树的魔法,如果你写:

    Func<string, int> func = s => s.Length;
    

    和写法一样:

    Func<string, int> func = delegate(string s) { return s.Length; };
    

    这也和写一样:

    int f(string s)
    {
        return s.Length;
    }
    
    Func<string, int> func = f;
    

    换句话说,第一个版本s =&gt; s.Length 只是上一个示例中命名 函数f匿名 版本。在 C# 中,这称为 Lambda 表达式

    你也可以这样写:

    Expression<Func<string, int>> expr = s => s.Length;
    

    请注意,语法与之前完全相同,我们只是将其分配给Expression&lt;Func&lt;string, int&gt;&gt;,而不是Func&lt;string, int&gt;。那么,您的问题本质上是,Expression 部分的作用是什么?

    最好的想法是:Func&lt;...&gt; 是一个可以运行的委托,并且已经编译了。 Expression&lt;Func&lt;...&gt;&gt; 是同一委托之前它被编译。这是编译器看到的。上面的表达式是一棵树,表示为:

    Lambda
       |
       +----> Member Access
       |        |
       |        +-----> Parameter (Name: "s", Type: System.String)
       |        |
       |        +-----> Member (Property: System.String.Length)
       |
       +----> Parameter (Name: "s", Type: System.String)
    

    这就是正式的Abstract Syntax Tree。 AST 是代码在解析之后但在编译之前的样子。事实上,每个Expression 实际上都有一个Compile 方法,您可以使用该方法将其编译成对应的可执行Func 类型。

    使用Expression&lt;...&gt; 而不是Func&lt;...&gt; 的原因通常是,当您不想要编译版本时,至少不是马上。很多时候,在这种情况下,您会使用一个,因为您只想获取属性名称(例如上面示例中的Length),但您希望在保持编译时类型安全的所有好处的同时这样做,而不是使用反射,如果某些类被更改,它可能会神秘地中断。

    在上述特定情况下,您可以使用以下代码获取属性名称:

    void Foo(Expression<Func<T, TResult>> expr)
    {
        var member = ((MemberExpression)expr.Body).Member;
        var memberName = member.Name;
        // Do something with the member and/or name
    }
    

    当然,此时您可以做几十种不同的事情,所以我不会尝试更多细节 - 您可以深入研究 MVC 源代码。此外,上面的示例不是生产就绪代码,因为它假设表达式是纯成员访问,但它可能更复杂,例如s =&gt; s.Length + 1,在这种情况下,上述方法将失败。顺便说一下,像这样的表达式在 Linq 或 MVC 中使用时会神秘地失败。

    希望这能回答您的问题。委托是一种传递函数的方式,而表达式是一种传递代码的方式。您通常不需要编写涉及Expression 的代码,除非您正在编写库或框架。但是,如果您正在编写库或框架,那么表达式树是一个非常强大的工具,它比老式的反射更安全,而且性能可能更高。

    【讨论】:

    • "事实上,C# 编译器可以将任何 Func 隐式转换成其等效的表达式。" - 它永远不可能。只有向后转换是可能的,即编译。
    • @jim:你到底在说什么?在这个答案中有一个明确的例子就在下面。自己尝试一下,你会看到它们都编译了。也许我不够精确,应该说,Func&lt;...&gt; literals 可以隐式转换为Expression&lt;Func&lt;...&gt;&gt;。您无法将Func&lt;...&gt; variable 转换为Expression&lt;Func&lt;...&gt;&gt;,但我认为此答案中的示例非常清楚地消除了歧义。换句话说,每个 lambda 表达式都可以分配给 Func&lt;...&gt;Expression&lt;Func&lt;...&gt;&gt;
    • 尝试编译它:Func f = x => x;表达式> ex = f;
    • @jim:我不是这个意思,我在之前的评论中清楚地解释了这一点。我不认为你读过它。如果它让你感觉好些,我在答案中添加了一个词,这使得该陈述在技术上 100% 正确。
    • @JohnH:很多表达式在 Linq to SQL 或 Entity Framework 等库中是无效的,因为它们需要被翻译成 SQL 查询,而且不能。在 Linq to Objects 中,它们将始终有效。当然,MVC 通常需要纯属性表达式,你不能写Html.TextBoxFor(x =&gt; x.FirstName + " " + x.LastName)
    【解决方案2】:

    Type Expression&lt;Func&lt;TModel, TProperty&gt;&gt; 是“一条指令,如何处理 TModel 类型的传入对象以返回其他 TProperty 类型的对象”。这里:

    public static MvcHtmlString TextBoxFor<TModel, TProperty>
        (this HtmlHelper<TModel> htmlHelper, 
              Expression<Func<TModel, TProperty>> expression)
    {
        return htmlHelper.TextBoxFor(expression, format: null);
    }  
    

    HtmlHelper的TextBoxFor扩展方法需要像上面描述的这样的参数,但是表达式参数与创建HtmlHelper的类型相同,即一些TModel,它需要返回TProperty类型的对象。
    实际参数m =&gt; m.SomeValue 等于“只返回传入 m 的 SomeValue 属性”,但也可以是“返回“foo”或“返回 null”或“返回新 WeirdObject()”。
    方法TextBoxFor 只调用重载方法。
    UPDATE
    首先,关于“泛型类型”的 MSDN 和 google 文章(如 thisthis)比我能更好地解释事情。使用这种机制,您可以创建特定于在创建实例时提供的类型的类或方法,以便它们能够返回相同类型的结果,而不是一般的“对象”。
    假设我想创建一个 List 类(在 Microsoft 之前),它可以保存任何类型的值列表,并且还可以将它们中的每一秒作为结果返回。我可以这样做:

    public class List{
        public IEnumerable Items; // collection of "objects"
        public IEnumerable GetEvenItems(){
        // some implementation returning another "objects" collection
        }
    }
    

    您可以使用它来维护整数、字符串或性别或其他任何内容的列表,但 GetEvenItems 将始终返回“对象”,您需要将它们转换回原始类型以继续更具体地工作。取而代之的是,我制作了另一个课程:

    public class List<T>{ }
    

    并且通过这句话“程序员必须指定所需的类型,以便它在类中始终是已知的,因此我可以随时将值转换为它”。正如我现在知道的类型,我可以使用它。例如:

    public class List<T>{
        public T[] Items; // collection of strongly typed values
        public T[] GetEvenItems(){
            // some implementation returning typed collection
        }
    }
    

    我的意思是说,现在的项目将是特定类型的,这是在创建时提供的。此外,我的 Item 和 GetEvenItems 也返回特定类型的项目,因此我可以像对整数或字符串的集合一样对它们进行操作。当外部代码调用 myList.GetEvenItems 时,它知道该方法将返回 T 数组。顺便说一下,您可以使用任何其他名称来代替 T,这只是一个“类型变量”。您可以在声明中使用 TModelTMyThoughts 而不是 T
    我还可以限制可能的类型。假设我的方法 DoTheWebJob 可以操作只能是 IController 类型的东西。然后我提供一个额外的约束:

    public class MyClass<T> where T: IController
    {
        public T[] Items;
        public void DoTheWebJob()
        {
            Items[0].Execute(null);
        }
    }
    

    这意味着可以为我的班级指定唯一的 IController 后代。由于我的班级主体已经“知道” TIController,因此我可以轻松调用 IController 特定方法。
    你也可以设计你的类或方法,这样程序员就必须提供不止一种这样的类型:

    public class List<T1, T2>{ }
    

    到目前为止一切顺利。放手

    System.Web.Mvc.HtmlHelper<T>
    

    这就像我们的List&lt;T&gt;.:创建实例时,程序员指定实际的T值如下:

    HtmlHelper<int> myHelper = new HtmlHelper<int>();
    

    假设我想拥有自己的渲染 html 标签的助手。

    public class MyHtmlHelper<T>
    {
        public string RenderSpan(string name, object value)
        {
            return String.Format("<span id=\"{0}\">{1}</span>", name, value.ToString());
        }
    }
    

    它很酷,可以使用参数中提供的namevalue 渲染SPAN 标签。所以我可以放任何东西,它会给我一个好看的SPAN,我什至根本不需要在类声明中使用泛型。
    现在我想修改我的渲染器,以便将SPANID 属性设置为某个对象的属性名称。假设我有具有 Id 属性的 Product 对象。我想将 Product 传递给渲染器,以便将 SPANID="Id" 和内部 html 设置为 Id 的值(说5)。我的渲染器如何知道属性 Id 的名称?如果我只是传递 Product.Id,这将只是一个整数值,渲染器将不知道这个属性名称是什么,并且无法设置 SPAN ID=...
    好吧,表达的力量会帮助我们。首先:Func&lt;T1, T2&gt; 是一个接受 T1 类型参数并返回 T2 类型结果的委托。 Expression&lt;Func&lt;T1, T2&gt;&gt; 是一个描述委托 Func&lt;T1, T2&gt; 逻辑的表达式 - 所以它可以很容易地编译成委托本身,但不能向后编译。

    写下这段代码:

    internal class Program
    {
        public class Entity
        {
            public int Id { get; set; }
        }
        private static void Main(string[] args)
        {
            Expression<Func<Entity, int>> fn = e => e.Id;
            // breakpoint here
        }
    }
    

    设置断点并观察 fn.Body 数据类型。它将属于 PropertyExpression - 简而言之,系统将 e =&gt; e.Id 解析为“获取对象的此 属性”,而不是“返回 Id 值”。因为现在 body “认为” this 作为某个对象的属性,并且能够读取它的名称和值。使用这样的表达式,我们可以让渲染器知道我们的属性名称,这样它就可以渲染SPAN

    public class MyHtmlHelper<T>
    {
        public string RenderSpan(string name, object value)
        {
            return String.Format("<span name=\"{0}\">{1}</span>", name, value.ToString());
        }
        public string RenderSpan(System.Linq.Expressions.PropertyExpression pe)
        {
            // extract property name and value and render SPAN here
    
        }
        public string RenderSpan(Expression<Func<object, object>> expr)
        {
            // if specified expr was like x => x.Id then it will actually be parsed like PropertyExpression in above
        }
    }
    

    但我们在类声明中已经有了实体类型 T 并且可以使用这种类型。所以我们可以修改最后一个方法:

    public string RenderSpan(Expression<Func<T, object>> expr)
    {
        // if specified expr was like x => x.Id then it will actually be parsed like PropertyExpression in above
    }
    

    这意味着如果 htmlHelper 是按照 HtmlHelper&lt;MyModel&gt; 类型创建的,那么 RenderSpan 将需要 Expression&lt;Func&lt;MyModel, object&gt;&gt; 表达式。例如:myModel =&gt; myModel.Id;。在 cshtml 文件中,您尝试创建 TextBoxFor 并会看到它需要与 @model 相同的类型。这是因为实际的 html 帮助程序被隐式创建为 new HtmlHelper&lt;MyModel&gt;()。现在,当 RenderSpan 知道 T 是创建 HtmlHelper 的类型时,它可以允许您使用 T 的属性在x =&gt; x.Id 的右侧。它知道 T = Entity,你可以说“x.Id”。如果表达式是object, object,那么您将无法执行此操作。在 Microsoft 的声明中,他们使用 TModel 而不是 T 来让您直观地了解它的含义。
    好的,现在简而言之:
    1.你在你的cshtml中写@model MyModel
    2. MVC 创建一个HtmlHelper&lt;MyModel&gt; 助手
    3. 由于#2 SpanForTextBoxFor 知道表达式传入参数是MyModel 类型,可以对其属性进行操作,并允许您在表达式的右侧使用它键入 我不确定他们为什么需要第二个类型参数作为 TProperty,它可能只是 object。可能它在 TextBoxFor 方法中传播得更深。

    【讨论】:

    • 感谢您的回答。我应该说我大致了解什么是表达。但我无法捕捉到&lt;TModel, TProperty&gt; 部分:既不是在表达式方法定义部分,也不是在表达式参数中......
    • @Agat 他们来自 HtmlHelper 声明。顺便说一句,我更新了答案。
    • 感谢您的努力,所有这么长的解释可能对既不熟悉泛型也不熟悉表达式的人有所帮助。但不幸的是(或高兴地)那不是我。正如我之前已经提到的,我知道所有这些事情(包括知道 MVC 是如何工作的)。但是这里唯一我仍然没有得到的东西(如果要说我的第一个代码:TextBoxFor&lt;TModel, TProperty&gt;)什么是 TProperty,为什么它在扩展方法定义中,为什么它作为参数从声明中返回表达式。
    • 您正确地触摸到了 html 助手基本上是在 TModel 上创建的,但是关于 TProperty 呢?
    • @Agat 我认为它只是在 TextBoxFor 中引入,以便在实现中可能使用。 TextBoxFor 可能会为指定的属性运行内部泛型方法。
    【解决方案3】:

    所以,边教边学。这就是我们的生活。呵呵

    这个问题最初似乎澄清了我对发布的代码不清楚的一些具体事情,但经过所有调查,我想,分享我对泛型、扩展方法和表达式的全部理解是有道理的(如果它们一起使用)。

    这很“吓人”,但从其他人的手来看C#也是如此美妙,当这样一个简单的方法调用时:

    Html.TextBoxFor(model => model.SomeValue)
    

    在自身内部隐藏了这样的范围和“深度”声明,例如:

    public static class InputExtensions
    {
        public static MvcHtmlString TextBoxFor<TModel, TProperty>
            (this HtmlHelper<TModel> htmlHelper, 
             Expression<Func<TModel, TProperty>> expression)
        {
            string format = (string) null;
            return InputExtensions
                    .TextBoxFor<TModel, TProperty>(htmlHelper, expression, format);
        }
    }
    

    提供的代码是使用 Resharper 的“Go to Implementation”收到的反编译版本。 (其实升级到 Resharper 8 后我必须使用“Navigate To/Decompiled Sources”,不过这里不讨论——以防万一)。

    因此,我将尝试解释此类方法定义的整个一般“解剖”(至少,我对此有所了解)。


    目标:

    拥有一个扩展方法,该方法可以扩展某些泛型类并允许操作该类数据(在编译时:具有 Intellisense 和重构的所有好处),具体取决于已将哪个类作为类型参数传递给提到的泛型类.

    在现实生活中的解释中,我会这样描述:我们有一个类Car(通用类),它(通常)“适合”携带不同的东西,如牛奶、卷心菜、自行车。但是当我们定义像Car&lt;Milk&gt; 这样的东西时,那辆车只能运载牛奶。此外,我们认为该类构建已经完成(我们无法更改它)。但是,我们还想购买一些拖车,它可以携带与已经为某些汽车定义的产品完全相同的产品,例如具有“CreateATrailer”方法。我们能够确定我们需要的拖车类型的方法是使用产品(在我们的例子中为Milk)测量单位(因为不同产品的测量单位不同:垃圾、公斤、物品)。在这种情况下,通用扩展方法(可能使用表达式)非常方便。 (这可能不是一个理想的现实生活示例,但这是我想到的)。

    简而言之:

    public static class InputExtensions
    // ^^ this is where Extension Methods must be placed (inside of a static class)
    {
        public static MvcHtmlString TextBoxFor<TModel, TProperty>
        //     ^^ they must also be static    ^^ here must be defined all the generic types
        //                                       which are involved withing the method
    
            (this HtmlHelper<TModel> htmlHelper, 
        //   ^^ the first parameter must have "this"
        //      this is a parameter which defines the type that the method operates on
        //      so, in this case, it must be some "HtmlHelper<TModel>" class instance
    
             Expression<Func<TModel, TProperty>> expression)
        //   ^^ the second parameter in the declaration, 
        //      but the first one which appears from caller side (the only one in this very case)
        {
            string format = (string) null;
    
            return InputExtensions.TextBoxFor<TModel, TProperty>
                   (htmlHelper, expression, format);
            // or might also be (dependently if the types 
            //    can be resolved automatically by compiller (the explanation below))
            //    as follows:
            return InputExtensions.TextBoxFor
                   (htmlHelper, expression, format);
        }
    }
    

    深潜:

    我不会重复genericsextension methodsexpressions (actually, initially it's called "expressions trees") 的所有详细说明,这些说明可通过 google 和 msdn 广泛获得。但将更多地关注这一切如何协同工作。

    public static class InputExtensions
    

    没有什么具体的。只是任何静态类。它的名字大多不起任何作用。


        public static MvcHtmlString TextBoxFor<TModel, TProperty>
    

    方法必须是静态的。

    如果我们不使用泛型,我们将完全省略 <...> 部分。但是如果我们这样做,我们应该在那里指定所有类型(类型模板),哪些运行时类型将由编译器自动解析(取决于您在调用方法时传递的参数),或者必须明确定义,如@987654332 @。


            (this HtmlHelper<TModel> htmlHelper, 
    

    “this”修饰符必须放在第一个参数旁边,this is实际上是方法是扩展的标志

    htmlHelper 参数将表示对象,我们在其上调用扩展方法。为了更容易理解,可以简单地将其替换为“@this”名称,例如(this HtmlHelper&lt;TModel&gt; @this。唯一的区别是您显然无权访问该类的任何私有成员(与要扩展的类的“内部”相反)。

    这里只是一个通用的泛型类型原型——没有什么特别的。它可以是我们想要的任何东西。甚至string,即(this string @this,


             Expression<Func<TModel, TProperty>> expression)
    

    这对我来说是最棘手的部分。

    所以,如果说到扩展方法部分,这将是您在扩展某些类时需要提供给方法调用的第一个参数。

    至于表达式......我们在这里使用它来允许用户传递一些值,这些值可以进一步(通过内部方法)从我们提供给他的实例中获取。即,Func&lt;TModel, TProperty&gt;(通常与Expression&lt;Func&lt;TModel, TProperty&gt;&gt; 相同(但通常不完全相同))意味着,在方法调用中,我们为用户提供了一些使用该类型的能力,该类型用于我们的HtmlHelper 实例化为 Type 参数。

    换句话说,如果我们创建了一个Car&lt;Milk&gt; 的实例(在我们的例子中Milk 表示TModel),那么我们将向调用者提供Milk 类型,例如Html.TextBoxFor(ourKindOfMilkObject =&gt; ourKindOfMilkObject.MeasureUnits)

    (我想,对于不太熟悉表达式(甚至是 Func/Action 概念)的人来说,这可能很难理解,所以我只是希望如果您阅读本文,您已经知道它是什么(至少,基本上))。


    这里最棘手的问题是TProperty,为什么这里甚至需要它。

    好的,它是如何工作的:

    • 首先,必须定义某种类型以从Func (Exspression&lt;Func&gt;) 语句返回。如果您不太在意,那也可能只是 object 类型:Expression&lt;Func&lt;TModel, TProperty&gt;&gt; expression;
    • 如果您改用object(并且不使用TProperty,但仍将其保留在方法名称声明中),则TProperty运行时类型将无法解析,您应该在方法上显式解析调用(这显然在开发中没有意义);
    • 如果你离开它并且在调用方法时有类似的表达式作为传递:Html.TextBoxFor(model =&gt; model.SomeValue),编译器解析TProperty类型,你不必在方法调用上指定它的类型,否则您必须执行以下操作:Html.TextBoxFor&lt;ATypeOfYourModel, string&gt;(model =&gt; model.SomeValue)(在这种情况下为string == TProperty)。
    • TProperty 的运行时类型被解析后,您会在 VisualStudio 工具提示(或 Resharper 提示)中将语法高亮显示为 '(this HtmlHelper htmlHelper, Expression> expression)'

    • 这里最重要的区别还在于我们可以指定可用于TProperty Prototype 的类型限制(下面的“AND:”部分)。


    关于方法调用,再说一遍。如果我们的对象如下(例如):

    var html = new HtmlHelper<CustomModel>();
    var car = new Car<Milk>();
    

    那么我们必须这样调用我们的扩展方法:

    html.TextBoxFor<CustomModel, string>(model => model.SomeValue);
    car.AddATrailer<Milk,ParticularMeasureUnitsType>
                 (theCarProduct => theCarProduct.MeasureUnits);
    

    但是如果所有通用类型模板运行时类型都已解析(在我们的例子中是这样的(因为Car 知道它是由其定义(new Car&lt;Milk&gt;())创建的Milk 和表达式Func 返回值类型也可以从SomeValue类型定义),那么我们简化一下:

    html.TextBoxFor(model => model.SomeValue);
    car.AddATrailer(theCarProduct => theCarProduct.MeasureUnits);
    

    与:

    关于泛型的非常重要的一点是,我们可以使用关键字where 来定义可用于泛型类型模板的类/接口的限制,例如:

    (this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
            where TModel : CustomModel where some : ParticularValueType
    

    否则,如果我们使用object而不是TProperty,我们将无法控制我们可以传递和不能传递给方法的内容(同理,在哪些方法上允许调用扩展方法等等)哪个不)。


    我相信,这里可能还有一些需要改进或更正的地方,所以,请给你的 cmets 提供这方面的信息 -- 我很乐意修改主题。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-11-30
      • 2021-12-04
      • 2012-12-14
      • 2012-10-18
      • 2013-07-10
      • 2012-07-21
      • 1970-01-01
      相关资源
      最近更新 更多