2009-06-03 7 views
9

Учитывая следующий пример кода:Плюсы и минусы «новых» свойств в C#/.Net?

// 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.

Теперь, когда дано, что CustomerOrders должна быть доставлена ​​с использованием стратегии ParcelDelivery, мы могли бы рассмотреть «нового» ИНГ свойства доставки в классе CustomerOrder:

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, очевидно, должен обеспечить совместимость (полиморф) с заказом)

Это позволяет прямое использование стратегии ParcelDelivery на CustomerOrder без необходимости кастинга.

Не могли бы вы использовать этот шаблон? почему, почему нет?

Обновление: Я нашел этот шаблон вместо использования дженериков, потому что я хочу использовать его для нескольких свойств. Я не хочу использовать аргументы общего типа для всех этих свойств.

+2

Пока ваш метод затенения не имеет более строгих предварительных условий и не имеет более слабых постусловий, я считаю, что эта практика вполне приемлема. –

+0

В первую очередь, почему вам нужно было лить DeliveryStrategy в любом случае? Точка использования шаблона стратегии заключается в том, что поведение реализует интерфейс и поэтому имеет все те же методы, поэтому его не нужно отличать. –

ответ

6

Я думаю, что это хороший образец.Это упрощает явное использование производных типов, устраняя необходимость приведения результата и не «ломает» поведение базового класса. На самом деле, подобная структура используется в некоторых классах в BCL, например, посмотреть на класс DbConnection иерархии:

  • DbConnection.CreateCommand() возвращает DbCommand
  • SqlConnection.CreateCommand() скрывает базовую реализацию с использованием 'new', чтобы вернуть SqlCommand.
  • (другие реализации DBConnection сделать то же самое)

Таким образом, если вы манипулируете объект соединения через переменную DbConnection, CreateCommand возвращает DbCommand; если вы манипулируете им через переменную SqlConnection, CreateCommand вернет SqlCommand, избегая приведения, если вы назначаете его переменной SqlCommand.

+0

Принято как ответ, для показа образных случаев из .Net-структуры (хотя они не используют «новый» в SqlConnection!) –

+1

«хотя они не используют« новое »в SqlConnection»: на самом деле я почти уверен, что они do: это единственный способ определить метод с тем же именем и параметром, что и в базовом классе, но с другим типом возвращаемого значения. Я думаю, вы проверили Reflector, но код, показанный Reflector, не является фактическим исходным кодом: он может показывать только то, что было фактически создано в IL, и, по-видимому, «новый» модификатор не испускает никакого IL (или, по крайней мере, Reflector doesn показать это). –

+0

использование нового в исходном коде просто запрещает компилятору предупреждать вас о том, что он делает это для вас в любом случае (это может измениться, конечно) – ShuggyCoUk

5

Вы можете использовать дженерики.

// 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()) 
    { } 
} 
11

Я бы предпочел, чтобы сделать тип родовым:

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 

Ouch.

Я рассматриваю new как обычно в качестве последней инстанции. Он вводит дополнительную сложность как с точки зрения реализации, так и использования.

Если вы не хотите идти по общему маршруту, я бы представил совершенно новое свойство (с другим именем).

+0

Я не могу дождаться C# 4 с контравариантностью в обратных типах. – erikkallen

+0

Ковариантные типы возврата, контравариантные типы параметров :) Однако, это только для интерфейсов и только в определенных сценариях, насколько мне известно ... –

+0

@Jon: я сделал защитник свойств защищенным для этого точного случая. Базовый класс и производный класс должны работать вместе, чтобы снять это. –

1

Есть ли причина, по которой вам необходимо изменить тип возврата? Если нет, то я бы предложил просто сделать свойство доставки виртуальных поэтому он должен быть определен унаследованных классов вместо:

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; } 
} 

Если вам требуется изменение типа возвращаемого значения, то я бы интересно, почему вы бы необходимо, чтобы это изменило поведение при обратном типе. Независимо, если это так, то это не сработает для вас.

Чтобы ответить на ваш вопрос, я бы использовал только описанный вами шаблон, если требуется, чтобы тип возвращаемого значения отличался от базового класса и очень экономно (я бы проанализировал мою объектную модель, чтобы увидеть, есть ли что-то еще, что я мог сделать в первую очередь). Если это не так, то я бы использовал шаблон, описанный выше.

3

Использование ключевого слова 'новое', чтобы скрыть свойства, доступные для записи, из базового класса - это плохая идея, на мой взгляд. Новое ключевое слово позволяет скрыть элемент базового класса в производном классе, а не переопределять его. Это означает, что вызовы таких членов, использующих базовый класс, по-прежнему получают доступ к коду базового класса, а не к производному классу. C# имеет ключевое слово «virtual», которое позволяет производным классам фактически выполнять реализацию, а не просто скрывать ее. Существует достаточно хорошая статья here, в которой говорится о различиях.

В вашем случае, похоже, вы пытаетесь использовать метод, скрывающий как способ введения property covariance в C#. Однако есть проблемы с этим подходом.

Зачастую значение наличия базового класса позволяет пользователям вашего кода обрабатывать типы полиморфно. Что происходит с вашей реализацией, если кто-то задает свойство Delivery, используя ссылку на базовый класс? Будет ли производный класс нарушен, если свойство Delivery НЕ является экземпляром ParcelDelivery? Вот те вопросы, которые вам нужно задать себе в этом выборе.

Теперь, если свойство Delivery в базовом классе не предоставило сеттер, тогда у вас есть немного другая ситуация. Пользователи базового класса могут только получить свойство и не устанавливать его. Поскольку вы перенаправляете свой ресурс обратно в базовый класс, доступ к базовому классу продолжает работать. Однако, если ваш производный класс не запечатан, классы, которые наследуют его, могут вводить одни и те же проблемы, скрывая свойство Delivery с собственной версией.

Как уже упоминалось в некоторых других сообщениях, вы можете использовать generics как способ достижения разных типов свойств доставки. Пример Джона довольно хорош, демонстрируя это. Существует одна проблема с подходом дженериков, если вам нужно вывести из CustomerOrder и изменить свойство Delivery на новый тип.

Существует альтернатива дженерикам. Вам нужно подумать о том, действительно ли вы хотите найти подходящее свойство в своем случае. Если вы избавитесь от сеттера в свойстве «Доставка», проблемы, возникающие при использовании класса Order, будут уходить прямо. Поскольку вы устанавливаете свойство доставки с помощью параметров конструктора, вы можете гарантировать, что все заказы имеют правильную стратегию.

1

Рассмотрим этот подход:

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} 
    } 
} 

Это еще далеко от совершенства (ваш пример не показывает почему базовый класс должен знать о стратегии доставки на всех, и было бы более разумным иметь общий характер с ограничением, но это, по крайней мере, позволяет вам использовать одно и то же имя для этого свойства и получать информацию о типе безопасности.

Как и в вашем примере, было бессмысленно, если что-то всегда не правильный тип, вы не должны замаскировать его нулевым, вы должны бросить, когда ваш инвариант был нарушен.

поля только для чтения всегда предпочтительнее, когда это возможно. Они делают неизменность ясной.

+0

Мне нравится альтернатива, предлагаемая интерфейсами, но я считаю, что этот шаблон делает модель более сложной (с двумя свойствами доставки в одном классе). –

+0

В отсутствие несоответствия co/contra вы должны сделать это самостоятельно либо на стороне потребителя (разные имена/роли), либо у поставщика (два свойства, как у меня, так и на тени). Во всех сценариях поставщика будут два свойства (вы просто не видите его с помощью «нового») – ShuggyCoUk

1

Ваше решение не делает то, что вы думаете. Кажется, он работает, но он не вызывает ваш «новый» метод. Рассмотрим следующие изменения в свой код, чтобы добавить некоторый вывод, чтобы увидеть, какой метод называется:

// 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; } 
    } 
} 

Тогда следующий фрагмент кода на самом деле использовать класс CustomOrder:

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

вывел бы «Заказать ». Новый метод специально разрушает полиморфизм. Он создает новое свойство Delivery на CustomOrder, которое не является частью базового класса 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; } 
    } 
} 
+0

Это то, что я ожидаю от него. Измените Console.WriteLine («Заказ») на Console.WriteLine (delivery.GetType(). GetName()) (или некоторые из них), и вы увидите ;-) –

+0

delivery.GetType() предоставит вам ParcelDelivery, потому что вы передал его конструктору Order, а не потому, что вы вызываете новое свойство Delivery для подкласса. Если вы хотите использовать отражение, чтобы увидеть, где вы находитесь, какой метод вы на самом деле вызываете, измените строку записи на: Console.WriteLine (System.Reflection.MethodBase.GetCurrentMethod(). DeclaringType.FullName); – ZombieDev

0

Использование new теневым виртуальных членов базового класса является плохой идеей, потому что суб-производные типы не будут иметь возможность переопределить их должным образом. Если существуют классы, которые нужно будет теневизировать в производных классах, члены базового класса не должны быть объявлены как abstract или virtual, но вместо этого вместо этого нужно просто называть или protected virtual. Производный тип может теневой метод базового класса с тем, который вызывает соответствующий член protected и соответствующим образом выполняет результат.