为什么.NET List Sort()不采用显式声明的委托对象?

时间:2014-05-15 17:44:38

标签: c# sorting generics delegates

快速提问。

this documentation page的第二个例子中(第二个代码块,以一个名为CompareDinosByLength的方法为特色),Sort方法被这样调用:

dinosaurs.Sort(CompareDinosByLength);

为什么Sort方法不需要显式声明的委托,正如我在阅读委托文档时所想到的那样?在我找到这个例子之前,我试图这样做:

delegate int CompareDinosDel(string first, string second);
CompareDinosDel newDel = CompareDinosByLength;
dinosaurs.Sort(newDel);

但是我不断得到与委托/委托方法相关的错误,而不是正确的比较者。

两者都不应该起作用吗?

3 个答案:

答案 0 :(得分:4)

  

为什么Sort方法不需要显式声明的委托?

C#允许方法组 - 也就是说,在没有(...)参数列表的情况下命名的方法来调用它 - 在委托中使用的上下文中使用预期。编译器在方法组上执行重载解析,就好像方法组已使用委托的形式参数类型的参数调用一样。这决定了应该使用方法组方法来创建委托。

当方法组对委托类型进行重载解析时,这种重载解析过程有时会导致涉及方法类型推断的异常情况,委托类型是泛型方法的形式参数类型;幸运的是,Sort不是一种通用方法,所以这些奇怪之处并没有发挥作用。

此功能已添加到C#2.0;在此之前,必须通过

将方法组转换为委托
new MyDelegate(MyMethod)
  

我不断收到与委托/委托方法相关的错误Comparer。两者都不应该起作用吗?

不幸的是,没有。 C#在委托类型上没有结构标识。那就是:

delegate void Foo();
delegate void Bar();
...
Foo foo = ()=>{};
Bar bar = foo; // ERROR!

即使FooBar 在结构上相同,编译器也不允许转换。但是,您可以使用上一个技巧:

Bar bar = foo.Invoke;

这相当于

Bar bar = new Bar(foo.Invoke);

然而,新的bar具有调用foo的操作;它经历了间接的程度。

此功能确实有意义。

原因一:

您不希望结构标识在其他地方起作用:

struct Point { int x; int y; ... }
struct Pair { int key; int value; ... }
....
Point point = whatever;
Pair pair = point; // ERROR

原因二:

您可能想说:

delegate int PureMethod(int); 

并且有一个PureMethod委托是“纯粹”的约定 - 也就是说,它们所代表的方法不抛出,总是返回,返回仅从其参数计算的值,并且不产生副作用。说

应该是错误的
Func<int, int> f = x => { Console.WriteLine(x); return x+1; };
PureMethod p = f;

因为f不纯粹。

然而事后看来,人们实际上并没有制造充满语义的代表。令人痛苦的是,Predicate<int>类型的值无法分配给Func<int, bool>类型的变量,反之亦然。

如果我们不得不重新做一遍,我怀疑代表们在CLR中会有结构性身份。

最后,我注意到VB对于分配混合委托类型更加宽容;如果需要,它会自动构建适配器委托。这可能会让人感到困惑,因为有时看起来它仍然保留了引用标识,但事实并非如此,但这符合VB“只是让我的代码工作”的理念。

答案 1 :(得分:2)

请考虑以下代码:

public class Foo
{
    public int Bar { get; set; }
}

public class SomeOtherFoo
{
    public int Bar { get; set; }
}

我应该说:

Foo foo = new SomeOtherFoo();

这也不适用于C#。当您有两种具有相同主体/实现的不同类型时,它们仍然是不同的类型。具有相同属性的两个类仍然是不同的类。具有相同签名的两个不同代表仍然是不同的代表。

Sort方法已经定义了委托类型,您需要匹配它。这非常类似于定义一个需要作为参数接受的类;你不能只使用相同的属性和方法传递另一种类型。

这就是静态输入语言的含义。另一种类型的系统是使用“Duck Typing”,其中语言不应用变量属于特定类型的约束,而是使用一组特定的成员。换句话说,“如果它像鸭子一样走路,像鸭子一样呱呱叫,假装它是鸭子。”这与打字的风格相反,即“它必须是一只鸭子,时期,即使它知道如何走路和嘎嘎。”

答案 2 :(得分:2)

dinosaurs.Sort(CompareDinosByLength);

CompareDinosDel newDel = CompareDinosByLength;
dinosaurs.Sort(newDel);
  

不应该都有效吗?

不,因为你将两个非常不同的东西传递给这两个函数调用。

这里的关键是要认识到,在这两种情况下,您实际传递给方法的是委托。在第一种情况下,编译器会隐式为您创建一个正确类型的委托,即使您没有明确要求它。在第二种情况下,您正在制作自己的委托,但它的类型错误,因此尝试将失败。

从.NET 2.0开始,C#编译器允许您在许多情况下跳过显式创建委托。如果在期望委托的上下文中使用方法名称,并且编译器可以验证方法签名和委托签名是否匹配,则它将使用该方法隐式构造委托实例。也就是说,而不是这样做(&#34;旧&#34;方式)

this.SubmitButton.Click += new System.EventHandler(this.SubmitButton_Click);

您现在可以执行此操作:

this.SubmitButton.Click += this.SubmitButton_Click;

Visual Studio本身仍然会生成较旧的语法,我认为因为它仍然有效,并且因为它不值得开发人员花时间去搞乱它。但是,如果您在自己的代码中使用它,大多数流行的代码分析工具都会标记冗余的委托创建。

这种技术适用于任何有方法的地方(技术上是&#34;方法组&#34;,因为一个方法名称可以引用多个重载),并将它分配给委托类型的变量。将方法作为参数传递到另一个方法是相同类型的赋值操作:您是&#34;分配&#34;调用站点的实际参数是方法体中的形式参数,因此编译器也做同样的事情。换句话说,以下两个方法调用完全相同:

dinosaurs.Sort(CompareDinosByLength);
dinosaurs.Sort(new Comparison<string>(CompareDinosByLength));

另一方面,你作为代表的不成功尝试做了一些不同的事情:

dinosaurs.Sort(new CompareDinosDel(CompareDinosByLength));

这一次,您告诉编译器您想要什么样的委托,但该方法所期望的委托类型。一般来说,编译器不会试图再次猜测你告诉它做了什么;如果你要求它做一些外观&#34; fishy&#34;,它会产生错误(在这种情况下,类型不匹配错误)。

此行为类似于您尝试执行此操作时会发生的情况:

public class A
{ 
   public int x;
}

public class B
{ 
   public int x;
}

public void Foo(A a) { }

public void Bar()
{
    B b = new B();
    this.Foo(b);
}

在这种情况下,AB是两种不同的类型,即使它们的&#34;类型签名&#34;是完全一样的。适用于A的任何代码行也可以在B上同样有效,但是,我们不能互换使用它们。代表类似于任何其他类型,C#的类型安全规则要求我们在需要的地方使用正确的委托类型,并且只能使用< em>足够接近类型。

这是一件好事的原因是因为委托类型可能具有更多意义,只是它的技术组件意味着它。与任何其他数据类型一样,当我们为应用程序创建委托时,我们通常会对这些类型应用某种语义含义。例如,我们期望,如果我们有ThreadStart委托,那么它将与新线程启动时运行的方法相关联。委托人的签名就像你得到的一样简单(没有参数,没有返回值),但代表背后的含义非常重要。

正因为如此,我们通常希望编译器告诉我们是否尝试在错误的位置使用错误的委托类型。通常情况下,这可能表明我们即将做一些可能编译甚至运行的事情,但可能会做错误。从来没有你想要的程序。


虽然这一切都是真的,但通常你真的不想为你的代表分配任何语义,或者,你的应用程序的其他部分分配了意义,这也是事实。 。有时你真的只想传递一段必须在以后运行的任意代码。这在函数式程序或异步程序中非常常见,您可以在其中获得诸如continuation,callbacks或用户提供的谓词之类的东西(例如,查看各种LINQ方法)。为此,.NET 3.5及更高版本在ActionFunc系列中提供了一组非常有用的完全通用委托。