【问题标题】:What is the purpose of hiding (using the "new" modifier) an interface method declaration?隐藏(使用“new”修饰符)接口方法声明的目的是什么?
【发布时间】:2010-08-13 12:18:53
【问题描述】:

可以将接口中的方法声明标记为“new”,但它是否具有任何“技术”意义,或者它只是一种明确声明该声明不能覆盖前一个声明的方式?

例如:

interface II1
{
    new void F();
}

interface II2 : II1
{
    new void F();
}

是有效的(C# 4.0 编译器不会抱怨)但似乎与:

interface II1
{
    void F();
}

interface II2 : II1
{
    void F();
}

提前感谢您提供任何信息。

编辑:你知道隐藏在界面中有用的场景吗?

编辑: 根据此链接:Is method hiding ever a good idea(感谢 Scott), 最常见的场景似乎是协变返回类型的仿真。

【问题讨论】:

    标签: c# interface new-operator


    【解决方案1】:

    第二个示例发出以下编译器警告:

    'II2.F()' 隐藏继承的成员 'II1.F()'。使用 new 关键字 if 隐藏是故意的。

    我想说使用 new 关键字的不同之处在于:显示意图。

    【讨论】:

    • 感谢您的回答。所以它不会影响接口的使用方式吗?我将编辑最初的帖子以澄清这一点。
    • @Serious:不,唯一的区别是编译器警告。为了清楚起见,我建议使用 new 关键字。好吧,不,我实际上建议不要一般隐藏方法,因为我觉得它令人困惑,但如果需要,请使用new 关键字。
    • 你说得对,我也没有看到任何需要它的有趣用例。
    • @Serious,它有几个用例。见stackoverflow.com/questions/2663274/…
    • @Scott,非常感谢这个链接,我已经相应地更新了最初的帖子。
    【解决方案2】:

    通读这些答案后,他们很好地解释了 new 运算符的作用,但我看不到对 OP 问题的这一部分的任何明确答案:

    你知道隐藏在界面中会很有用的场景吗?

    总而言之,这一切都归结为可测试性和重用性。通过将接口拆分成更小的块,并遵守Interface Separation Principle,我们可以使我们的类的用户最小地依赖无关的细节并最大程度地解耦,这为我们提供了更多的可重用性选项和更轻松的测试时间。

    一般来说,当我们需要以不可避免的方法冲突的方式分支接口类型层次结构时,new 运算符会在这里发挥作用。这一切听起来有点抽象,很难解释,所以我创建了一个我认为是最小的示例,我们希望将接口类型层次结构一分为二,同时有一个通用的共享方法。我把代码放在 .NET fiddle 上:

    https://dotnetfiddle.net/kRQpoU

    又来了:

    using System;
    
    public class Program
    {
        public static void Main()
        {
            //Simple usage
            var simpleCuboid = new MockCuboidSimple();
            var heightUser = new HeightUserEntangled();
            var volumeUser = new VolumeUserEntangled();
            Console.WriteLine("*** Simple Case ***");
            Console.WriteLine(heightUser.PrintHeight(simpleCuboid));
            Console.WriteLine(volumeUser.PrintVolume(simpleCuboid));
    
            //Smart usage - the same behaviour, but more testable behind the scenes!
            var smartCuboid = new MockCuboidSmart();
            var heightUserSmart = new HeightUserSmart();
            var volumeUserSmart = new VolumeUserSmart();
            Console.WriteLine("*** smart Case ***");
            Console.WriteLine(heightUserSmart.PrintHeight(smartCuboid));
            Console.WriteLine(volumeUserSmart.PrintVolume(smartCuboid));
        }
    }
    
    //Disentangled
    
    class VolumeUserSmart
    {
        public string PrintVolume(IThingWithVolume volumeThing)
        {
            return string.Format("Object {0} has volume {1}", volumeThing.Name, volumeThing.Volume);
        }       
    }
    
    class HeightUserSmart
    {
        public string PrintHeight(IThingWithHeight heightThing)
        {
            return string.Format("Object {0} has height {1}", heightThing.Name, heightThing.Height);
        }       
    }
    
    class MockCuboidSmart : ICuboidSmart
    {
        public string Name => "Mrs. Cuboid";
        public double Height => 3.333;
        public double Width => 31.23432;
        public double Length => 123.12;
        public double Volume => Height * Width * Length;
    }
    
    interface ICuboidSmart : IThingWithHeight, IThingWithVolume
    {
        //Here's where we use new, to be explicit about our intentions
        new string Name {get;}
        double Width {get;}
        double Length {get;}
        //Potentially more methods here using external types over which we have no control - hard to mock up for testing
    }
    
    interface IThingWithHeight
    {
        string Name {get;}
        double Height {get;}
    }   
    
    interface IThingWithVolume
    {
        string Name {get;}
        double Volume {get;}
    }
    
    //Entangled
    
    class VolumeUserEntangled
    {
        public string PrintVolume(ICuboidSimple cuboid)
        {
            return string.Format("Object {0} has volume {1}", cuboid.Name, cuboid.Volume);
        }       
    }
    
    class HeightUserEntangled
    {
        public string PrintHeight(ICuboidSimple cuboid)
        {
            return string.Format("Object {0} has height {1}", cuboid.Name, cuboid.Height);
        }       
    }
    
    class MockCuboidSimple : ICuboidSimple
    {
        public string Name => "Mrs. Cuboid";
        public double Height => 3.333;
        public double Width => 31.23432;
        public double Length => 123.12;
        public double Volume => Height * Width * Length;
    }
    
    interface ICuboidSimple
    {
        string Name {get;}
        double Height {get;}
        double Width {get;}
        double Length {get;}
        double Volume {get;}
        //Potentially more methods here using external types over which we have no control - hard to mock up for testing
    }
    

    请注意,VolumeUserSmartHeightUserSmart 仅依赖于它们关心的 ICuboidSmart 接口的片段,即 IThingWithHeightIThingWithVolume。这样,它们可以最大程度地重复使用,例如对于长方体以外的形状,也可以更容易地测试。最后一点是,我在实践中发现,至关重要。用更少的方法来模拟一个接口要容易得多,尤其是当主接口类型中的方法包含对我们无法控制的类型的引用时。当然,总是可以通过一个 mocking 框架来解决这个问题,但我更喜欢保持代码干净的核心。

    那么new 关键字在哪里适合?好吧,既然VolumeUserSmartHeightUserSmart 都需要访问Name 属性,我们必须在IThingWithHeightIThingWithVolume 中声明它。因此我们必须在子接口ICuboidSmart 中重新声明它,否则我们会得到一个编译器错误,抱怨歧义。在这种情况下,我们正在做的是隐藏在IThingWithHeightIThingWithVolume 中定义的Name 的两个版本,否则它们会发生冲突。而且,就像其他答案指出的那样,虽然我们没有必须在这里使用new,但我们应该明确说明我们隐藏的意图。

    【讨论】:

      【解决方案3】:

      Fredrik 回答后的示例。我希望有一个接口,它表示通过 id 获取实体的方法。然后,我希望 WCF 服务和其他一些标准存储库都可以用作该接口。为了玩 WCF,我需要用属性来装饰我的界面。从意图的角度来看,我不想用 WCF 的东西来装饰我的界面,因为它不会只通过 WCF 使用,所以我需要使用 new。代码如下:

      public class Dog
      {
          public int Id { get; set; }
      }
      
      public interface IGetById<TEntity>
      {
          TEntity GetById(int id);
      }
      
      public interface IDogRepository : IGetById<Dog> { }
      
      public class DogRepository : IDogRepository
      {
          public Dog GetById(int id)
          {
              throw new NotImplementedException();
          }
      }
      
      [ServiceContract]
      public interface IWcfService : IGetById<Dog>
      {
          [OperationContract(Name="GetDogById")]
          new Dog GetById(int id);
      }
      

      【讨论】:

      • 有趣,它来自真实的用例吗?
      • 是的,我所有的存储库检索方法都抽象在一个接口后面(每个方法一个)。然后任何需要支持某种检索的对象都可以实现该接口。其中一些是导致上述代码的 WCF 服务。
      【解决方案4】:

      我几乎在每个界面中都使用它。看这里:

      interface ICreature
      {
          /// <summary>
          ///     Creature's age
          /// </summary>
          /// <returns>
          ///     From 0 to int.Max
          /// </returns>
          int GetAge();
      }
      
      interface IHuman : ICreature
      {
          /// <summary>
          ///     Human's age
          /// </summary>
          /// <returns>
          ///     From 0 to 999
          /// </returns>
          new int GetAge();
      }
      

      拥有一个要求较少但义务较大的继承成员是绝对正常的。没有违反 LSP。但如果是这样,在哪里记录新合同?

      【讨论】:

      • 不确定在这种情况下创建new 方法是最合适的方法。对我来说,不同年龄段的人应该由实现内部管理,每个实现都会记录它们的不变量。如果我继续你的例子,并不是所有的人都有相同的范围:族长确实有一个 0-999 的范围,但从那时起,范围就更多了 0-149。正如你所说,这种合约不能用语言或运行时本身来描述,但你可以使用像 System.ComponentModel.DataAnnotations.RangeAttribute 这样的元数据。
      • 这不是关于范围,而是关于要求和义务。范围只是需求的最简单示例。在实现中管理这些 req/obl 会导致重复/不可重用的代码。在这样一个简单的例子中并不明显,但在具有深层次结构的大型项目中是不可避免的。并非所有人都有相同的范围 - 这取决于您的领域。在我的抽象世界里——这是真的。在你的 - 做你自己的抽象;)
      • 是的,但并非所有要求都应以这种方式表达。再举一个例子,如果我有一个接口VehiclegetNumberOfWheels 方法,则没有理由重新定义新方法,因为自行车有2,汽车有4。如果他们做一些功能不同的事情,重新定义新方法很重要。
      • 是的,在IBike 的情况下,如果您可以毫无问题地接受IVehicle 的广泛合同,则没有理由重新定义此方法。无论如何,您将在 class Bike : IBike 中实现此方法,您可以在其中指定新的、更严格的义务,因为每个类也有自己的契约。
      • 这个例子没有意义。接口IHuman 没有做任何事情来执行更严格的要求。事实上,如果您从IHuman 中删除new int GetAge(),那么代码的后果将是零。无论哪种方式,此类都相同:public class Human : IHuman { public int GetAge() { return 0; } }
      【解决方案5】:

      两者是非常不同的。通过使用“新”,您正在创建一个新的继承链。这意味着II2 的任何实现都需要实现F() 的两个版本,而您最终调用的实际版本将取决于引用的类型。

      考虑以下三种实现:

          class A1 : II1
          {
              public void F()
              {
                  // realizes II1.F()
              }
          }
      
          class A2 : II2
          {
              void II1.F()
              {
                  // realizes II1.F()
              }
      
              void II2.F()
              {
                  // realizes II2.F()
              }
          }
      
          class A3 : II2
          {
              public void F()
              {
                  // realizes II1.F()
              }
      
              void II2.F()
              {
                  // realizes II2.F()
              }
          }
      

      如果您引用了A2,您将无法调用F() 的任何一个版本,除非先转换为II1II2

      A2 a2 = new A2();
      a2.F(); // invalid as both are explicitly implemented
      ((II1) a2).F(); // calls the II1 implementation
      ((II2) a2).F(); // calls the II2 implementation
      

      如果您引用了A3,您将能够直接调用II1 版本,因为它是一个隐式实现:

      A3 a3 = new A3();
      a3.F(); // calls the II1 implementation
      ((II2) a3).F(); // calls the II2 implementation
      

      【讨论】:

      • 请注意,问题中给出的两个示例之间的编译代码没有区别。您解释的区别是方法隐藏的效果,但无论是否使用 new 关键字都会发生这种情况。
      • 感谢这些优秀的例子。
      • @Fredrik:哎呀,谢谢。出于某种原因,我完全忽略了这一点。您确实是正确的,警告是唯一的区别。
      • @PaulRuane 您愿意更新答案以包含 Fredrik 的见解吗?
      【解决方案6】:

      我知道这有一个很好的用途:您必须这样做来声明一个从另一个 COM 接口派生的 COM 接口。在magazine article 中涉及到它。

      Fwiw,作者完全误解了问题的根源,它与“遗产税”无关。 COM 只是利用了典型 C++ 编译器实现多重继承的方式。每个基类都有一个 v-table,这正是 COM 所需要的。 CLR 不这样做,它不支持 MI,并且只有一个 v-table。接口方法被合并到基类的 v-table 中。

      【讨论】:

        【解决方案7】:

        new 修饰符非常简单,它只是抑制在隐藏方法时创建的编译器警告。如果用于不隐藏其他方法的方法,则会生成警告。

        来自The C# Language Specification 3.0

        10.3.4 新修饰符 类成员声明允许声明与继承成员具有相同名称或签名的成员。发生这种情况时,就说派生类成员隐藏了基类成员。隐藏继承的成员不被视为错误,但它确实会导致编译器发出警告。为了抑制警告,派生类成员的声明可以包含一个新的修饰符,以指示派生成员旨在隐藏基成员。该主题将在 §3.7.1.2 中进一步讨论。 如果新修饰符包含在不隐藏继承成员的声明中,则会发出警告。删除新修饰符会抑制此警告。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-12-01
          • 2013-08-09
          • 2015-11-24
          • 2016-03-25
          • 2010-09-14
          • 2011-11-03
          • 2012-05-05
          相关资源
          最近更新 更多