【问题标题】:Pros and cons of 'new' properties in C# / .Net?C#/.Net 中“新”属性的优缺点?
【发布时间】:2010-10-30 23:11:57
【问题描述】:

考虑以下示例代码:

// delivery strategies
public abstract class DeliveryStrategy { ... }
public class ParcelDelivery : DeliveryStrategy { ... }
public class ShippingContainer : DeliveryStrategy { ... }

以及以下示例订单类:

// order (base) class
public abstract class Order
{
    private DeliveryStrategy delivery;

    protected Order(DeliveryStrategy delivery)
    {
        this.delivery = delivery;
    }

    public DeliveryStrategy Delivery
    {
        get { return delivery; }
        protected set { delivery = value; }
    }
}

当我派生一种新类型的订单类时,它将继承 DeliveryStrategy 类型的 Delivery 属性。

现在,当必须使用 ParcelDelivery 策略交付 CustomerOrders 时,我们可以考虑在 CustomerOrder 类中为 Delivery 属性添加“new”:

public class CustomerOrder : Order
{
    public CustomerOrder()
        : base(new ParcelDelivery())
    { }

    // 'new' Delivery property
    public new ParcelDelivery Delivery
    {
        get { return base.Delivery as ParcelDelivery; }
        set { base.Delivery = value; }
    }
}

(CustomerOrder 显然需要确保与 Order 兼容(多态)

这允许直接在 CustomerOrder 上使用 ParcelDelivery 策略,而无需强制转换。

您会考虑使用这种模式吗?为什么/为什么不?

更新:我想出了这种模式,而不是使用泛型,因为我想将它用于多个属性。我不想为所有这些属性使用泛型类型参数

【问题讨论】:

  • 只要你的阴影方法没有更严格的前置条件和更弱的后置条件,我认为这种做法是完全可以接受的。
  • 首先,您为什么需要强制转换 DeliveryStrategy?使用策略模式的重点是行为实现了一个接口,因此具有所有相同的方法,因此不需要强制转换。

标签: c# .net properties new-operator


【解决方案1】:

使用new 隐藏基类的虚拟成员是一个坏主意,因为子派生类型将无法正确覆盖它们。如果存在想要在派生类中隐藏的类成员,则不应将基类成员声明为abstractvirtual,而应简单地调用protected abstractprotected virtual 成员。派生类型可以使用调用适当的protected 成员并适当地转换结果的方法来隐藏基类方法。

【讨论】:

    【解决方案2】:

    您的解决方案并没有像您认为的那样做。它似乎有效,但没有调用您的“新”方法。考虑对您的代码进行以下更改以添加一些输出以查看调用了哪个方法:

    // order (base) class
    public abstract class Order
    {
        private DeliveryStrategy delivery;
    
        protected Order(DeliveryStrategy delivery)
        {
            this.delivery = delivery;
        }
    
        public DeliveryStrategy Delivery
        {
            get 
            {
                Console.WriteLine("Order");
                return delivery; 
            }
            protected set { delivery = value; }
        }
    }
    
    public class CustomerOrder : Order
    {
        public CustomerOrder()
            : base(new ParcelDelivery())
        { }
    
        // 'new' Delivery property
        public new ParcelDelivery Delivery
        {
            get 
            {
                Console.WriteLine("CustomOrder");
                return base.Delivery as ParcelDelivery; 
            }
            set { base.Delivery = value; }
        }
    }
    

    然后下面的 sn-p 代码来实际使用您的 CustomOrder 类:

    Order o = new CustomerOrder();
    var d = o.Delivery;
    

    将输出“订单”。新方法专门打破了多态性。它在 CustomOrder 上创建一个新的 Delivery 属性,该属性不属于 Order 基类。因此,当您像使用 Order 一样使用 CustomOrder 时,不要调用新的 Delivery 属性,因为它只存在于 CustomOrder 上,而不是 Order 类的一部分。

    你想要做的是覆盖一个不可覆盖的方法。如果您的意思是该属性是可覆盖的,请将其抽象化:

    // order (base) class
    public abstract class Order
    {
        private DeliveryStrategy delivery;
    
        protected Order(DeliveryStrategy delivery)
        {
            this.delivery = delivery;
        }
    
        public abstract DeliveryStrategy Delivery
        {
            get { return delivery; }
            protected set { delivery = value; }
        }
    }
    
    public class CustomerOrder : Order
    {
        public CustomerOrder()
            : base(new ParcelDelivery())
        { }
    
        public override ParcelDelivery Delivery
        {
            get { return base.Delivery as ParcelDelivery; }
            set { base.Delivery = value; }
        }
    }
    

    【讨论】:

    • 确实如我所愿。将 Console.WriteLine("Order") 更改为 Console.WriteLine(delivery.GetType().GetName()) (或类似的),你会看到 ;-)
    • delivery.GetType() 将为您提供 ParcelDelivery,因为您将它传递给 Order 的构造函数,而不是因为您正在调用子类的新 Delivery 属性。如果您想使用反射来查看您所在的位置,您实际调用的是哪个方法,请将 writeline 更改为:Console.WriteLine(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName);
    【解决方案3】:

    考虑这种方法:

    public interface IOrder
    {
        public DeliveryStrategy Delivery
        {
            get;
        }
    }
    
    // order (base) class
    public abstract class Order : IOrder
    {
        protected readonly DeliveryStrategy delivery;
    
        protected Order(DeliveryStrategy delivery)
        {
            this.delivery = delivery;
        }
    
        public DeliveryStrategy Delivery
        {
            get { return delivery; }
        }
    }
    

    然后使用

    public class CustomerOrder : Order
    {
        public CustomerOrder()
            : base(new ParcelDelivery())
        { }
    
        public ParcelDelivery Delivery
        {
            get { return (ParcelDelivery)base.Delivery; }
        }
    
    
        DeliveryStrategy IOrder.Delivery
        {
            get { return base.Delivery}
        }
    }
    

    这仍然远非完美(您的示例没有显示为什么基类根本需要了解交付策略,并且具有约束的通用性会更有意义,但是这至少允许您为属性使用相同的名称并获得类型安全。

    在你的例子中是没有意义的,如果某些东西永远是不是正确的类型,你不应该用 null 来掩盖它,你应该抛出你的不变量已经被违反了。

    在可能的情况下,总是首选只读字段。它们清楚地表明了不变性。

    【讨论】:

    • 我喜欢接口提供的替代方案,但我相信这种模式会使模型更加复杂(在同一个类上有两个 Delivery 属性)。
    • 在没有 co/contra 差异的情况下,您必须自己在消费者端(不同的名称/演员)或提供者(两个属性相互喜欢我的或通过阴影)自己做提供者场景将有两个属性(你只是看不到'new')
    【解决方案4】:

    在我看来,使用“new”关键字从基类中隐藏可写属性是一个坏主意。 new 关键字允许您在派生类中隐藏基类的成员,而不是覆盖它。这意味着使用基类引用对此类成员的调用仍然访问基类代码,而不是派生类代码。 C# 有 'virtual' 关键字,它允许派生类实际 override 实现,而不是简单地隐藏它。有一篇相当不错的文章 here 讨论了这些差异。

    在您的情况下,您似乎正在尝试使用方法隐藏作为将 property covariance 引入 C# 的一种方式。但是,这种方法存在问题。

    通常,拥有基类的价值在于允许您的代码的用户以多态方式处理类型。如果有人使用对基类的引用设置 Delivery 属性,您的实现会发生什么情况?如果 Delivery 属性不是 ParcelDelivery 的实例,派生类会中断吗?对于这种实施选择,您需要问自己这些问题。

    现在,如果基类中的 Delivery 属性没有提供 setter,那么您的情况会略有不同。基类的用户只能检索属性而不能设置它。由于您将属性访问路由回基类,因此通过基类进行的访问将继续有效。但是,如果您的派生类不是密封的,则从它继承的类可能会通过使用自己的版本隐藏 Delivery 属性来引入相同类型的问题。

    正如其他一些帖子已经提到的,您可以使用泛型作为实现不同 Delivery 属性类型的一种方式。 Jon 的例子很好地证明了这一点。如果您需要从 CustomerOrder 派生并将 Delivery 属性更改为新类型,则泛型方法存在一个问题。

    有一个泛型的替代方案。您需要考虑您是否真的想要一个可设置的属性。如果去掉 Delivery 属性上的设置器,直接使用 Order 类引入的问题就消失了。由于您使用构造函数参数设置交付属性,因此可以保证所有订单都具有正确的策略类型。

    【讨论】:

      【解决方案5】:

      我认为这是一个很好的模式。通过消除转换结果的需要,它可以更容易地显式使用派生类型,并且不会“破坏”基类行为。实际上,在 BCL 中的某些类中使用了类似的模式,例如查看 DbConnection 类层次结构:

      • DbConnection.CreateCommand() 返回一个 DbCommand
      • SqlConnection.CreateCommand() 使用“new”隐藏基本实现以返回 SqlCommand。
      • (其他 DbConnection 实现也是如此)

      因此,如果您通过 DbConnection 变量操作连接对象, CreateCommand 将返回 DbCommand ;如果您通过 SqlConnection 变量对其进行操作,CreateCommand 将返回一个 SqlCommand,如果您将其分配给 SqlCommand 变量,则避免强制转换。

      【讨论】:

      • 被接受作为答案,用于显示 .Net 框架中的类似案例(尽管它们在 SqlConnection 中不使用“新”!)
      • "虽然他们不在 SqlConnection 中使用 'new'":实际上我几乎可以肯定他们这样做了:这是定义与基类中具有相同名称和参数的方法的唯一方法,但返回类型不同。我猜你检查过 Reflector,但 Reflector 显示的代码不是实际的源代码:它只能显示在 IL 中实际生成的内容,而且显然“new”修饰符不会发出任何 IL(或者至少 Reflector 不会'不显示它)。
      • 在源代码中使用 new 只会阻止编译器警告你它正在为你做这件事(当然这可能会改变)
      • new 无法强制执行。你可以在编译和发布你的派生类之后在基类中引入一个方法。
      【解决方案6】:

      您是否有任何理由需要更改返回类型?如果没有,那么我建议只将 Delivery 属性设为虚拟,因此它必须由继承的类来定义:

      public abstract class Order
      {
          protected Order(DeliveryStrategy delivery)
          {
              Delivery = delivery;
          }
      
          public virtual DeliveryStrategy Delivery { get; protected set; }
      }
      
      public class CustomerOrder : Order
      {
          public CustomerOrder()
              : base(new ParcelDelivery())
          { }
      
          public DeliveryStrategy Delivery { get; protected set; } 
      }
      

      如果您确实需要更改返回类型,那么我想知道为什么您需要对返回类型进行如此剧烈的行为更改。无论如何,如果是这样,那么这对您不起作用。

      因此,要直接回答您的问题,如果需要返回类型与基类不同,我只会使用您描述的模式,并且非常谨慎(我会分析我的对象模型以查看是否有某些东西否则我可以先做)。如果不是这种情况,那么我会使用我上面描述的模式。

      【讨论】:

        【解决方案7】:

        我更喜欢将类型设为泛型:

        public abstract class Order<TDelivery> where TDelivery : Delivery
        {
            public TDelivery Delivery { ... }
            ...
        }
        
        public class CustomerOrder : Order<ParcelDelivery>
        {
            ...
        }
        

        这确保了编译时的类型安全,而不是留给执行时。它还可以防止以下情况:

        CustomerOrder customerOrder = new CustomerOrder();
        Order order = customerOrder;
        order.Delivery = new NonParcelDelivery(); // Succeeds!
        
        ParcelDelivery delivery = customerOrder.Delivery; // Returns null
        

        哎哟。

        我认为new 通常是最后的手段。它在实现和使用方面都增加了复杂性。

        如果您不想走一般路线,我会介绍一个真正的新属性(使用不同的名称)。

        【讨论】:

        • 我等不及 C#4 的返回类型具有逆变性。
        • 协变返回类型,逆变参数类型 :) 但是,这仅适用于接口,并且据我所知仅在某些情况下...
        • @Jon:对于这个确切的情况,我使属性设置器受到保护。基类和派生类需要协同工作才能实现。
        • 在这种情况下,您可以保护整个属性,并让每个派生类声明自己的强类型属性。
        • 您也可以只删除属性上的设置器,只允许派生类型使用构造函数参数设置类型。在这种情况下,您必须确定可设置的 DeliveryStrategy 是否有用。
        【解决方案8】:

        您可以使用泛型。

        // order (base) class
        public abstract class Order<TDeliveryStrategy> where TDeliveryStrategy : DeliveryStrategy
        {
            private TDeliveryStrategy delivery;
        
            protected Order(TDeliveryStrategy delivery)
            {
                this.delivery = delivery;
            }
        
            public TDeliveryStrategy Delivery
            {
                get { return delivery; }
                protected set { delivery = value; }
            }
        }
        
        public class CustomerOrder : Order<ParcelDelivery>
        {
            public CustomerOrder()
                : base(new ParcelDelivery())
            { }
        }
        

        【讨论】:

          猜你喜欢
          • 2017-12-14
          • 2013-10-12
          • 1970-01-01
          • 2015-06-24
          • 2015-06-17
          • 1970-01-01
          • 2010-11-09
          • 2010-10-02
          • 2013-05-15
          相关资源
          最近更新 更多