RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 749392
Accepted
VladD
VladD
Asked:2020-11-25 18:11:12 +0000 UTC2020-11-25 18:11:12 +0000 UTC 2020-11-25 18:11:12 +0000 UTC

虚拟方法是否需要声明为受保护的?

  • 772

同事们,我不完全理解.NET 设计指南中的建议之一。

它说:

确实更喜欢受保护的可访问性而不是虚拟成员的公共可访问性。公共成员应通过调用受保护的虚拟成员来提供可扩展性(如果需要)。

类的公共成员应该为该类的直接消费者提供正确的功能集。虚拟成员被设计为在子类中被覆盖,并且受保护的可访问性是将所有虚拟可扩展点限制在可以使用它们的位置的好方法。

那是

更喜欢使虚拟成员(如方法)受保护而不是公开。类的公共成员必须通过调用受保护的虚拟成员来提供可扩展性(如果需要)。

类的公共成员应为客户端代码提供所需的正确功能。虚拟成员被设计为在后代类中被覆盖,并且安全访问是一种很好的技术,可以限制场所的可见性以仅扩展到将使用它的人。

读完这篇文章后,我仍然不明白如果将虚函数族声明为 public 会出现什么问题。例如,在这段代码中:

class Human : IDisposable
{
    IDisposable property = new Property();
    public virtual Dispose()
    {
        property.Dispose();
    }
}

class Spy : Human
{
    IDisposable spyGadgets = new SpyGadgets();
    public virtual Dispose()
    {
        base.Dispose();
        spyGadgets.Dispose();
    }
}

这样的代码可能有什么问题?试图警告我的文档是什么?如果这种情况一切正常,那么在什么情况下可能出现问题?

如果可能的话,给出一个有意义的代码示例(而不是类Foo和Bar)。


非常感谢您的回复!我很难选择打哪一个,因为所有的答案都很好,并且从不同的角度阐明了问题。

c#
  • 6 6 个回答
  • 10 Views

6 个回答

  • Voted
  1. Best Answer
    tym32167
    2020-11-26T03:15:05Z2020-11-26T03:15:05Z

    这些是对框架开发人员的建议。显然,框架开发人员将发布其框架的新版本。同样清楚的是,他们最重要的任务之一是尽可能保持向后兼容性。这意味着应尽可能限制使用其代码的客户端。也就是说,习惯于编写要继承的类的是我们(好吧,或者只有我是这样一个笨手笨脚的人)——但是如果我们正在编写一个框架,那么我们需要一个足够好的理由来使类被继承。您还需要一个充分的理由来使该方法成为虚拟方法。但是现在您将公共方法设为虚拟,现在客户可以继承、重载该方法并使用我们的框架运行他们自己的代码——我们无法再控制它了。我的意思是,通过使公共方法虚拟化,我们赋予客户决定权 我们的 API 将做什么,我们不能在不破坏向后兼容性的情况下改变任何东西。但是,通过将受保护的方法设为虚拟,我们不保证客户端将来如果我们的公共 API 发生更改,该方法不会变得不可用。因此,即使公共方法逻辑已更改,重载受保护方法的客户端仍保持向后兼容。

    我想我需要添加一个示例,虽然我不是示例大师:) 假设有以下类:

    public class CsvWriter1<T>
    {
        protected void WriteHeader(TextWriter stream);
        protected void WriteBody(IEnumerable<T> obj, TextWriter stream);
    
        protected virtual void WriteInternal(IEnumerable<T> obj, TextWriter stream)
        {
            WriteHeader(stream);
            WriteBody(obj, stream);
        }
    
        public void Write(IEnumerable<T> obj, TextWriter stream)
        {
            WriteInternal(obj, stream);
        }
    }
    
    
    public class CsvWriter2<T>
    {
        protected void WriteHeader(TextWriter stream);
        protected void WriteBody(IEnumerable<T> obj, TextWriter stream);
    
        public virtual void Write(IEnumerable<T> obj, TextWriter stream)
        {
            WriteHeader(stream);
            WriteBody(obj, stream);
        }
    }
    

    现在让我们想象一下,在我们精彩框架的下一个版本中,我们需要在没有标头的 CSV 中编写对象。也就是说,不再需要标头。而且你不能再覆盖它了。该怎么办?上课CsvWriter1很容易

    public class CsvWriter1<T>
    {
        protected void WriteHeader(TextWriter stream);
        protected void WriteBody(IEnumerable<T> obj, TextWriter stream);
    
        protected virtual void WriteInternal(IEnumerable<T> obj, TextWriter stream)
        {
            WriteHeader(stream);
            WriteBody(obj, stream);
        }
    
        protected void WriteInternalNew(IEnumerable<T> obj, TextWriter stream)
        {
            WriteBody(obj, stream);
        }
    
        public void Write(IEnumerable<T> obj, TextWriter stream)
        {
            WriteInternalNew(obj, stream);
        }
    }
    

    就班级而言,CsvWriter2我们处于水坑中。由于对它的指定更改将被认为是不兼容的,当然它会破坏客户端类的逻辑。

    • 19
  2. Sergey Teplyakov
    2020-11-30T12:59:56Z2020-11-30T12:59:56Z

    我在 2000 年代初在 Herb Sutter 遇到了非虚拟接口的想法,然后在我看来这是一个很棒的想法(这并不奇怪,我是用 C++ 编写的,这种做法非常流行原则上,甚至由行业名人描述)。

    这是他的(萨特)的文章——虚拟性——对这个主题的解释(在我在上一段中提到的那个帖子中找到):

    传统上,许多程序员习惯于使用公共虚函数编写基类,以直接同时指定接口和可定制的行为。例如,我们可以这样写:

    // Example 1: A traditional base class.
    //
    class Widget
    {
    public:
      // Each of these functions might optionally be
      // pure virtual, and if so might or might not have
      // an implementation in Widget; see Item 27 in [1].
      //
      virtual int Process( Gadget& );
      virtual bool IsDone();
      // ...
    };
    

    问题在于“同时”部分,因为每个虚函数都在做两项工作:指定接口,因为它是公共的,因此直接成为 Widget 向世界其他地方呈现的接口的一部分;它指定了实现细节,即内部可定制的行为,因为它是虚拟的,因此为派生类提供了一个挂钩来替换该函数的基本实现(如果有的话)。公共虚拟功能本质上具有两个显着不同的工作,这表明它没有很好地分离关注点,我们应该考虑一种不同的方法。

    当我 10 年前迁移到 .NET 时,我尝试使用这个成语,只是出于习惯。但随后,他开始在她身上得分。

    现在,我们可以分析这个建议。

    1. 在 .NET 世界中几乎没有人使用建议。甚至 .NET Framework 也给了它一个大大的赞许。
    2. 如果您将其视为模板方法的特例,则该建议是合理的。如果在基类中定义了某些行为:参数验证,基本功能,至少是一些东西。那么公共接口的非虚拟方法和补充功能的受保护方法的分离是有意义的。

    结论:在充分尊重框架设计指南和 Sutter 个人的情况下,但这个建议可以而且应该被评分。为了创造方法而创造方法是浪费时间。我已经很长时间没有使用这个建议了,因此我不记得任何一个问题。是的,框架的作者没有遵循相同的建议,也没有任何特殊问题(因此)。

    西南。@Denis Gladkiy 提出了这样的问题:

    在此案中是否会有任何反对 NVI 的论据?

    然后应该以更明确的方式回答:

    1. NVI 是额外的工作,这意味着不需要任何缺点。需要“for”参数,因为 YAGNI 和过早的概括。
    2. 除了 C++ 之外,我还没有看到任何行业关注这种模式。我的猜测是它起源于 .NET,因为它的作者来自 C++ 世界。
    3. .NET Framework 有许多违反此原则的示例,从 System.Object 到Stream类。
    4. 这个规则已经存在了很长时间,但是在 .NET Framework 中出现或扩展的新 API(相同的Stream.FlushAsync)仍然没有遵循它。如果这个原则是有用的,并且不遵循它会很糟糕,但是一个不受向后兼容性约束的新 API 已经遵循它了。
    5. 根据我自己的经验或同事的经验,除非在非虚拟方法中存在某些行为,否则我认为遵循这种模式没有意义。我理解我的经验和同事的经验是一种主观意见,但我很难想象违反这一原则实际上会导致严重问题的情况。

    我向框架设计指南的作者 Krzysztof Kwalina 提出了同样的问题——“他今天对这个建议有什么看法?”。这是他的答案。

    如果我今天编写指南,我会将其设为“考虑”指南。最初编写指南时,我们有很多机会主义 (VB) 客户要求我们简化可扩展性 API,例如将它们隐藏在公共表面区域。现在大多数 .NET 开发人员不会被公共虚拟成员吓到。

    • 17
  3. andreycha
    2020-11-26T03:37:17Z2020-11-26T03:37:17Z

    公共方法是类的契约。如果公共方法被声明为虚拟的,则意味着子类可以很容易地以违反约定的方式覆盖它。

    在受保护的方法的情况下,破坏合同并不是那么容易(有时是不可能的),因为主要逻辑是硬编码在具有扩展点的非虚拟公共方法中 - 那些相同的受保护虚拟方法。

    有时这种技术也可以用来防止健忘的程序员,这样当重写一个方法时,他们“不会忘记”调用底层逻辑。像这样的东西。

    所以是的,“更喜欢”。但它是首选。

    • 11
  4. Pavel Mayorov
    2020-11-25T18:31:38Z2020-11-25T18:31:38Z

    Dispose方法没有问题:它的逻辑非常简单,它的合约是微软一劳永逸的,以后不会改变。

    但通常在基类中有一些更复杂的算法,其扩展点在中间,而不是在边缘。这就是为什么我们需要一个单独的方法:

    interface IValidator 
    {
        ValidationResult Validate(object obj);
    }
    
    class TypedValidator<T> : IValidator
    {
        public ValidationResult Validate(object obj)
        {
            if (obj is T t)
                return Validate(t);
            else
                return ValidationResult.Fail($"Wrong object type");
        }
    
        protected virtual ValidationResult Validate(T obj) => ValidationResult.Ok;
    }
    

    如果在上面的示例中它是声明为虚拟的公共 Validate 方法,那么任何派生类都必须复制基类的逻辑。

    一个更简单的情况是防御性参数检查。在基类中检查一次参数就足够了 - 您不应该在每个类中编写相同的检查。

    class Foo
    {
         public virtual void Baz(object obj)
         {
             if (obj == null) throw new ArgumentNullException("obj");
    
             // ...
         }
    }
    
    class Bar: Foo 
    {
         public override void Baz(object obj)
         {
             if (obj == null) throw new ArgumentNullException("obj"); // Этой проверки могло бы не быть
    
             // ...
    
             base.Baz(obj);
         }
    }
    
    • 7
  5. rdorn
    2020-11-25T20:39:09Z2020-11-25T20:39:09Z

    我经常使用这种方法,但说实话,我以前从未遇到过这个建议。原因很简单,在某个类层次结构的开发之初,并不总是清楚(相反,总是不清楚)哪些公共方法代码将被共享,哪些需要在继承者中重新定义。因此,作为起点,公共方法只是简单地调用受保护的虚拟方法。将来,受保护方法中的通用代码移动到公共方法中,而特定代码保留在受保护方法中。如果结果,所有代码都保留在受保护方法中,并且只保留对该方法的调用在公共方法中,然后我们将所有代码转移到公共方法并使其成为虚拟方法。

    当然,您也可以采取其他方式——始终以单独的方法显示细节,但这通常需要我更多的关注和手动工作。

    在使用虚拟公共方法时,我从未见过任何真正的问题,例如在公共字段而不是属性或常量而不是只读属性的情况下。

    • 5
  6. Viacheslav Ivanov
    2020-12-01T02:58:06Z2020-12-01T02:58:06Z

    NVI 加 5 美分:NVI 和 C#

    在一个简单易懂的示例中,无法在代码中显示不遵循这一点的问题。开发和维护代码的复杂性问题。

    在我看来,这类似于理解备份的必要性

    人们分为两种类型 - 已经进行备份的人和尚未进行备份的人。

    (例如,许多人已经习惯性地进行备份,但并不是每个人都在测试他们的备份)和许多其他类似的事情。

    • 4

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    Python 3.6 - 安装 MySQL (Windows)

    • 1 个回答
  • Marko Smith

    C++ 编写程序“计算单个岛屿”。填充一个二维数组 12x12 0 和 1

    • 2 个回答
  • Marko Smith

    返回指针的函数

    • 1 个回答
  • Marko Smith

    我使用 django 管理面板添加图像,但它没有显示

    • 1 个回答
  • Marko Smith

    这些条目是什么意思,它们的完整等效项是什么样的

    • 2 个回答
  • Marko Smith

    浏览器仍然缓存文件数据

    • 1 个回答
  • Marko Smith

    在 Excel VBA 中激活工作表的问题

    • 3 个回答
  • Marko Smith

    为什么内置类型中包含复数而小数不包含?

    • 2 个回答
  • Marko Smith

    获得唯一途径

    • 3 个回答
  • Marko Smith

    告诉我一个像幻灯片一样创建滚动的库

    • 1 个回答
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Алексей Шиманский 如何以及通过什么方式来查找 Javascript 代码中的错误? 2020-08-03 00:21:37 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    user207618 Codegolf——组合选择算法的实现 2020-10-23 18:46:29 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5