1

任何人都可以对这里发生的事情提供一个清晰(且易于理解)的解释(通常是泛型、扩展方法和表达式):

    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 参数在这种情况下的工作方式。我知道泛型是如何工作的(通常是他),我也知道这是一个扩展方法(以及它们也是如何工作的),但无法理解表达式在 Html.TextBoxFor 方法中是如何处理(或处理等)的(相对于函数的 TModel 和 TProperty)。

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

谢谢!

编辑:

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

4

3 回答 3

2

你应该明白的第一件事是Expression<Func<...>>Func<...>本质上是一回事。事实上,C# 编译器可以将任何Func文字隐式转换为其等效的Expression.

A 的Func<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 => s.Length只是上一个示例中命名函数的匿名版本。在 C# 中,这称为Lambda 表达式f

你也可以这样写:

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

请注意,语法与以前完全相同,我们只是将其分配给 aExpression<Func<string, int>>而不是 a Func<string, int>。所以,你的问题是,本质上,这Expression部分是做什么的?

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

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

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

使用 anExpression<...>而不是 a的原因Func<...>通常是,当您不想要编译版本时,至少不是马上。很多时候,在这种情况下,您会使用一个,因为您只想获取属性名称(例如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 => s.Length + 1,在这种情况下,上述方法将失败。顺便说一下,当在 Linq 或 MVC 中使用这样的表达式时,它们也会神秘地失败。

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

于 2013-11-11T04:02:44.037 回答
1

TypeExpression<Func<TModel, TProperty>>是“如何处理 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 => m.SomeValue等于“只返回传入 m 的 SomeValue 属性”,但也可以是“return "foo"”或“return null”或“return new WeirdObject()”。
方法TextBoxFor只调用一个重载方法。
更新
首先,关于“泛型类型”的 MSDN 和谷歌文章(像这个这个)比我能更好地解释事情。使用这种机制,您可以创建特定于在创建实例时提供的类型的类或方法,以便它们能够例如返回相同类型的结果而不是一般的“对象”。
假设我想创建一个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<T>.: 在创建实例时,程序员像这样指定实际的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());
    }
}

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

写下这段代码:

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 => 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<MyModel>类型创建的,那么RenderSpan将需要Expression<Func<MyModel, object>>表达式。例如:myModel => myModel.Id;。在 cshtml 文件中,您尝试创建TextBoxFor并会看到它需要与原来相同的类型@model。这是因为实际的 html 帮助程序被隐式创建为new HtmlHelper<MyModel>(). 现在,当RenderSpan知道T是创建HtmlHelper时使用的类型时,它可以允许您在. 它知道T = Entity,你可以说“ x.Id ”。如果表达式是x => x.Idobject, object那么你将无法做到这一点。在微软的声明中,他们使用TModel而不是T来让你直观地理解它的含义。
好的,现在简而言之:
1. 你@model MyModel在你的 cshtml 中编写
2. MVC 创建一个HtmlHelper<MyModel>助手
3. 由于 #2SpanForTextBoxFor知道表达式传入参数是MyModel类型,可以使用它的属性进行操作并允许你使用它输入表达式的正确部分
我不确定他们为什么需要第二个类型参数作为TProperty,它可能只是object。可能它在TextBoxFor方法中传播得更深。

于 2013-11-11T03:21:23.167 回答
1

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

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

这很“吓人”,但从其他人的手来看,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 后我必须使用“导航到/反编译的源代码”,但这不是在这里讨论的地方——以防万一)。

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


目标:

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

在现实生活中的解释中,我将描述如下:我们有一个类Car(通用类),它(通常)“适合”携带不同的东西,如牛奶、卷心菜、自行车。但是当我们定义类似的东西时Car<Milk>,那辆车只能运载牛奶。此外,我们认为该类构建已经完成(我们无法更改它)。但是,我们还想购买一些拖车,它会携带与已经为某些汽车定义的产品完全相同的产品,例如具有“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);
    }
}

深潜:

我不会重复对泛型扩展方法表达式(实际上,最初它被称为“表达式树”)的所有详细解释,它们可以通过 google 和 msdn 广泛获得。但将更多地关注这一切如何协同工作。

public static class InputExtensions

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


    public static MvcHtmlString TextBoxFor<TModel, TProperty>

该方法必须是静态的。

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


        (this HtmlHelper<TModel> htmlHelper, 

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

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

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


         Expression<Func<TModel, TProperty>> expression)

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

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

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

换句话说,如果我们创建了一个Car<Milk>(在我们的例子中是MilkTModel) 的实例,那么我们将向调用者提供 Milk 类型Html.TextBoxFor(ourKindOfMilkObject => ourKindOfMilkObject.MeasureUnits),例如 。

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


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

好的,它是如何工作的:

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

  • 这里最重要的区别也是我们可以指定可用于TProperty原型的类型的限制(下面的“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);

但是如果所有 Generic Types Templates 运行时类型都被解析(在我们的例子中是这样(因为Car知道Milk由它的定义创建(new Car<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,我们将无法控制我们可以传递和不能传递给方法的内容(同样的方法,允许调用扩展方法和不允许调用的方法)。


我相信,这里可能还有一些需要改进或纠正的地方,所以,请对此发表评论——我很乐意修改这个话题。

于 2013-11-11T19:47:42.317 回答