【问题标题】:more advantages or disadvantages to delegate members over classic functions?委托成员比经典功能有更多的优点或缺点?
【发布时间】:2011-12-23 02:31:49
【问题描述】:
class my_class
{
    public int add_1(int a, int b) {return a + b;}
    public func<int, int, int> add_2 = (a, b) => {return a + b;}
}

add_1 是一个函数,而 add_2 是一个委托。但是,在这种情况下,代表可以扮演类似的角色。

由于先例和语言的设计,C# 方法的默认选择应该是函数。

不过,这两种方法各有利弊,所以我制作了一份清单。这两种方法还有更多的优点或缺点吗?

传统方法的优势。

  • 更传统的
  • 函数的外部用户请参阅命名参数 - 对于 add_2 语法 arg_n 和类型通常没有足够的信息。
  • 与 intellisense 配合使用效果更好 - ty Minitech
  • 使用反射 - ty Minitech
  • 使用继承 - ty Eric Lippert
  • 有一个“this” - ty CodeInChaos
  • 更低的开销、速度和内存 - ty Minitech 和 CodeInChaos
  • 在更改和使用功能方面无需考虑公共\私有。 - ty CodeInChaos
  • 较少动态,允许较少在编译时未知 - ty CodeInChaos

“委托类型字段”方法的优点。

  • 更加一致,不是成员函数和数据成员,只是数据成员而已。
  • 可以在外观和行为上像一个变量。
  • 将其存储在容器中效果很好。
  • 多个类可以使用同一个函数,就好像每个类都是成员函数一样,这将非常通用、简洁并且具有良好的代码重用性。
  • 直接在任何地方使用,例如作为本地函数。
  • 在与垃圾收集一起传递时可能效果很好。
  • 更动态,在编译时必须知道的更少,例如,可能存在在运行时配置对象行为的函数。
  • 好像封装了它的代码,可以组合和返工,msdn.microsoft.com/en-us/library/ms173175%28v=vs.80%29.aspx
  • 函数的外部用户会看到未命名的参数 - 有时这很有帮助,尽管能够命名它们会很好。
  • 可以更紧凑,在这个简单的示例中,例如可以删除返回值,如果有一个参数,也可以删除括号。
  • 滚动您自己的行为,例如继承 - ty Eric Lippert
  • 其他考虑因素,例如功能性、模块化、分布式(代码编写、测试或代码推理)等...

请不要投票关闭,这已经发生了,它已经重新打开。这是一个有效的问题,即使您认为委托方法没有太多实际用途,因为它与既定的编码风格有冲突,或者您不喜欢委托的优势。

【问题讨论】:

  • 你知道第二个例子不是函数吗?这是一个代表。
  • 如果你想返回 void 那么你使用Action&lt;&gt;而不是Func&lt;&gt;
  • 您在哪里看到此选项可供选择?
  • 我不明白为什么它被关闭了。关于设计类的实现细节时的竞争因素,这是一个完全合理的问题。 我经常在我自己的程序中使用这种技术,因为它具有真正的优势——也就是说,它让我可以很容易地决定函数中的哪些方法对它们应用了记忆。
  • @EricLippert:我同意这是一个很好且有趣的问题。但它可能不符合 SO 准则。这种性质的问题会导致讨论、主观答案和部分答案,这有利于结束。

标签: c# class function delegates abstraction


【解决方案1】:

首先,对我来说,关于这个设计决策的“高阶位”是我永远不会用 public 字段/方法做这种事情。至少我会使用 property,甚至可能不会。

对于私有字段,我经常使用这种模式,通常是这样的:

class C
{
    private Func<int, int> ActualFunction = (int y)=>{ ... };
    private Func<int, int> Function = ActualFunction.Memoize();

现在我可以非常轻松地测试不同记忆策略的性能特征,而无需更改 ActualFunction 的文本。

“方法是委托类型的字段”策略的另一个优点是,您可以实现与我们“融入”语言的代码共享技术不同的技术。委托类型的受保护字段本质上是一个虚拟方法,但更灵活。派生类可以用他们想要的任何东西替换它,并且您已经模拟了一个常规的虚拟方法。但是您可以构建自定义继承机制;例如,如果您真的喜欢原型继承,您可以约定如果该字段为空,则调用某个原型实例上的方法,以此类推。

methods-are-fields-of-delegate-type 方法的一个主要缺点是,当然,重载不再有效。字段名称必须唯一;方法只是在签名上必须是唯一的。此外,您不会像我们获得泛型方法那样获得泛型字段,因此方法类型推断停止工作。

【讨论】:

  • 我曾经试图通过定义一个具有多个 Invoke 方法的委托类型(在 IL 中)来解决重载的缺点。它实际上可以工作,但不幸的是 C# 不适用于委托语法,并且需要为该类显式调用 .Invoke...
  • 我喜欢提及属性而不是字段,但这还不足以支持投票。是否还有其他缺点会使您“甚至可能不[使用财产]”?你说你会选择什么,但你没有提到很多理由。我在回答中对此进行了抨击:)
【解决方案2】:

在我看来,第二个比第一个绝对没有优势。它的可读性要低得多,效率可能较低(鉴于必须隐含Invoke)并且根本不简洁。更重要的是,如果你曾经使用过反射,它就不会显示为一种方法,所以如果你这样做来替换每个类中的方法,你可能会破坏一些看起来应该有效的东西。在 Visual Studio 中,IntelliSense 不会包含该方法的描述,因为您不能将 XML cmets 放在委托上(至少,与将它们放在普通方法上的方式不同)并且您不知道它们是什么无论如何都指向,除非它是只读的(但是如果构造函数改变了它呢?)并且它将显示为一个字段,而不是一个方法,这很令人困惑。

您真正应该使用 lambda 的唯一情况是在需要闭包的方法中,或者当它提供显着的便利优势时。否则,您只会降低可读性(基本上是我的第一段与当前段落相比的可读性)并破坏与 C# 以前版本的兼容性。

【讨论】:

  • 我很困惑。你什么时候会面临在函数和委托之间做出选择?它们用于完全不同的事情。这就像说+ 运算符比= 赋值更有用。
  • @Hogan:不确定你指的是什么——OP,还是答案?具体是哪一行? OP 意味着选择。这个答案回应了 OP 对选择的考虑。
  • -1 “令人困惑”、“可读”是高度主观的标准。我碰巧发现它们完全可读,而且我想大多数函数式程序员阅读它们都没有问题,甚至可能更喜欢它们。如果您碰巧如此依赖反射,那么您将面临比方法与函数更大的问题。
  • 对不起,@MauricioScheffer,但这是一个非常糟糕的理由。这就像说没有理由使用换行符或缩进你的代码,因为空格的可读性是高度主观的。 -1 到那个。
  • @minitech :您似乎将可读性与混淆混淆了。
【解决方案3】:

为什么默认情况下应避免将委托作为方法,以及有哪些替代方法:

学习曲线

以这种方式使用委托会让很多人感到惊讶。不是每个人都能理解委托,或者你为什么要换掉函数。似乎有一条学习曲线。一旦你通过它,代表看起来很简单。

性能和可靠性

以这种方式调用委托会降低性能。这是我默认使用传统方法声明的另一个原因,除非它在我的模式中启用了一些特殊功能。

还有一个执行安全问题。公共字段可以为空。如果你传递了一个带有公共字段的类的实例,你必须在使用它之前检查它是否不为空。这会伤害性能并且有点蹩脚。

您可以通过将所有公共字段更改为属性来解决此问题(无论如何,这是所有 .Net 编码标准中的规则)。如果有人试图分配 null,则在 setter 中抛出 ArgumentNullException

程序设计

即使您可以处理所有这些问题,允许方法完全可变也违背了静态 OO 和函数式编程语言的许多设计。

在静态 OO 中,类型始终是静态的,动态行为是通过多态实现的。您可以根据其运行时类型了解类型的确切行为。这对于调试现有程序非常有帮助。允许在运行时修改您的类型会损害这一点。

在静态 OO 和函数编程范式中,限制和隔离副作用 is quite helpful 以及使用 fully immutable structures 是实现此目的的主要方法之一。将方法公开为委托的唯一目的是创建可变结构,这具有完全相反的效果。

替代品

如果您真的想始终使用委托来替换方法,您应该使用像 IronPython 这样的语言或其他构建在 DLR 之上的语言。这些语言将针对您尝试实现的范例进行工具和调整。您的代码的用户和维护者不会感到惊讶。

话虽如此,有些用途证明使用委托代替方法是合理的。您不应该考虑这个选项,除非您有一个令人信服的理由这样做,以解决这些性能、混乱、可靠性和设计问题。只有在得到回报时才应该这样做。

用途

...示例代码:

public class Context
{
    private Func<int, int, int> executeStrategy;

    public Context(Func<int, int, int> executeStrategy) {
        this.executeStrategy = executeStrategy;
    }

    public int ExecuteStrategy(int a, int b) {
        return executeStrategy(a, b);
    }
}
  • 我发现了一个特殊情况,我认为公共委托属性是有保证的:使用实例而不是派生类来实现 Template Method Pattern...

...这在需要大量设置/拆卸的自动化集成测试中特别有用。在这种情况下,将状态保存在旨在封装模式而不是依赖于单元测试夹具的类中通常是有意义的。通过这种方式,您可以轻松地支持在夹具之间共享测试套件的骨架,而无需依赖(有时是粗制滥造的)测试夹具继承。它也可能更适合并行化,具体取决于测试的实现。

var test = new MyFancyUITest
{
    // I usually name these things in a more test specific manner...
    Setup = () => { /* ... */ },
    TearDown = () => { /* ... */ },
};
test.Execute();

智能感知支持

函数的外部用户看到未命名的参数 - 有时这很有帮助,尽管能够命名它们会很好。

使用命名委托 - 我相信这至少会让您获得一些 Intellisense 参数(可能只是名称,不太可能是 XML 文档 - 如果我错了,请纠正我):

public class MyClass
{
    public delegate int DoSomethingImpl(int foo, int bizBar);
    public DoSomethingImpl DoSomething = (x, y) => { return x + y; }
}

【讨论】:

  • @alan2here:编辑以完善这个答案作为建议,而不是让读者自己得出结论。还想到了另一个与方法即委托范式相匹配的用例,而不是作为公共字段。
  • 在调用委托之前不必检查空值,只有当它可能为空时,调用成员可能会导致代码运行然后失败。
  • @alan2here:公共字段可能为空,除非您可以保证它们不是。每当您从代码中的其他地方传递一个实例时,该字段可能为空,因此您必须进行空检查。您唯一可以跳过检查的情况是您是否在当前方法中构造了类的实例。普通方法本身不能为空,但字段可以。使用普通方法,您必须检查对象是否为空,但不是每个方法。
  • 除非在某些情况下,在某些程序中可能是每种情况,可以有意义地处理它,如果您检查会发生什么,并且委托方法结果为空,那比空异常?顺便说一句,我已经更新了问题,您现在可能更喜欢介绍性句子。
  • @alan2here:我对此有一个反驳,但我认为我们会陷入困境。我的主要观点是标准方法永远不能为空,而您可以将字段清空这一事实可能会导致问题。解决此问题的另一种方法是使用公共属性包装私有字段,在 setter 中检查 null,然后将 ArgumentNullException 扔在那里。我将编辑我的答案以推荐这个并退出“总是必须检查”的声明。
【解决方案4】:

我会避免将委托属性/字段作为公共方法的方法替换。对于私有方法,它是一种工具,但不是我经常使用的工具。

  • 实例委托字段具有每个实例的内存成本。对于大多数类来说可能是过早的优化,但仍然需要牢记。

  • 您的代码使用公共可变字段,可以随时更改。这会损害封装性。

  • 如果使用字段初始值设定项语法,则无法访问 this。所以字段初始化语法主要用于静态方法。

  • 使静态分析更加困难,因为该方法的实现在编译时是未知的。

在某些情况下委托属性/字段可能有用:

  • 某种处理程序。特别是如果多播(以及事件订阅模式)没有多大意义
  • 分配的东西不能用简单的方法体轻松描述。比如一个记忆函数。
  • 委托是运行时生成的,或者至少其值仅在运行时确定

【讨论】:

  • 你的输入,很有帮助。
【解决方案5】:

对局部变量使用闭包是使用方法和私有字段的替代方法。我非常不喜欢有很多字段的类,特别是如果其中一些字段仅由两种或更少的方法使用。在这些情况下,在字段中使用委托可能比传统方法更可取

class MyClassConventional {
  int? someValue; // When Mark() is called, remember the value so that we can do something with it in Process(). Not used in any other method.
  int X;

  void Mark() {
    someValue = X;
  }

  void Process() {
    // Do something with someValue.Value
  }
}

class MyClassClosure {
  int X;

  Action Process = null;

  void Mark() {
    int someValue = X;
    Process = () => { // Do something with someValue };
  }
}

【讨论】:

    【解决方案6】:

    这个问题提出了一种错误的二分法——在函数和具有等效签名的委托之间。主要区别在于只有在没有其他选择的情况下才应该使用两者之一。在您的日常工作中使用它,它将被排除在任何代码审查之外。

    上面提到的好处远远被几乎没有理由编写如此晦涩的代码的事实所抵消;尤其是当这段代码看起来你不知道如何编写 C# 时。

    我敦促任何阅读本文的人忽略已陈述的任何好处,因为他们都被这样的代码证明你不知道如何用 C# 编程的事实所淹没。

    该规则的唯一例外是,如果您需要其中一项福利,并且无法以任何其他方式满足该需求。在这种情况下,您需要编写比代码更多的注释来解释为什么您有充分的理由这样做。准备好像 Eric Lippert 一样清楚地回答。您最好能够像 Eric 那样解释您无法同时完成您的要求并编写可理解的代码。

    【讨论】:

    • -1 这个答案没有任何价值。代码审查和失业警告可能适用于你,但原则上我对这类代码没有任何问题;所以我不会一概而论。 “默默无闻”的论点再次非常适用于你;不要一概而论。我一点也不觉得这个晦涩难懂,而且我想许多函数式程序员都会非常适应它。 “你不知道如何用 C# 编程。” -> 像 C# 这样的主流语言功能越来越强大,习惯吧。见lambda-the-ultimate.org/node/1406
    • 您忽略此选项的方式并不能说明任一选项的优缺点。 为什么在这种情况下方法比委托更可取? 为什么是“默认”选项之一?仅仅因为你已经在 C# 中扎根于某种编码风格并不意味着它是唯一的方法。不过,我同意你论点的主旨。做委托有缺点,这些缺点将超过它们作为 默认 选项的使用。像这样使用它们应该是一个例外,而不是默认的做生意的方式,因此需要证明是合理的。
    猜你喜欢
    • 2011-08-24
    • 1970-01-01
    • 2018-10-31
    • 1970-01-01
    • 2012-05-09
    • 2014-02-08
    • 2012-04-05
    • 1970-01-01
    • 2023-03-28
    相关资源
    最近更新 更多