Как может сработать противоречивость аргументов в принципе Лискова?

Основным принципом подстановки Лискова является то, что суперкласс может быть заменен на подкласс, который следует тому же контракту (поведению). Или, как сказал Мартин Фаулер: "Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом".

Упоминается, что противоречивость аргументов является частью LSP, но я не могу понять, почему и как это может работать. Т.е. в подклассе переопределяющий метод может принимать более широкий (менее производный аргумент).

что-то вроде этого:

class Base
{
  int GetLenght(string s)
  {
     return s.lenght;
  }  
}
class Derived: Base
{
 override int GetLenght(object s)
  {
    ??  I cannot return any lenght of an object..
  }
}

Как это могло сработать? Я имею в виду, как я могу выполнить договор, если менее производный аргумент не обладает нужными мне свойствами?

PS: я знаю, что большинство ОО-языков не поддерживают это, мне просто любопытно.

3 ответа

Решение

Давайте немного изменим пример:

Давайте на минутку предположим, что здесь интерфейс Sequence который реализует GetLength метод, и это String реализует этот интерфейс. Допустим также, что в вашем примере вместо объекта Sequence используется, который является более широким типом, чем String (его фактическая реализация в этом случае).

Base base;
Derived derived;

String s;
Sequence o;

int i;
i = base.GetLength(s); // valid
i = derived.GetLength(o); // valid
i = base.GetLength(o); // obviously invalid

base = derived;
base.GetLength(s); // valid
base.getLength(o); // still invalid, 
// the contract of "Base" still requires an argument of type string,
// despite actually being of type "Derived"

Ваша фактическая реализация не имеет значения, важны типы. До тех пор, пока вы не нарушаете функциональность при получении строки, вы можете возвращать все, что плавает на вашей лодке, как длину произвольных объектов, например:

class Derived : Base {
    override int GetLenght(Sequence s) {
      return s.GetLength();
    }
}

Как видите, вы можете дать derived любой тип Sequence, но Base по-прежнему требует определенного типа String,

Таким образом, во многих случаях контравариантность не нарушает LSP. Как вы видите на собственном примере, вы не можете использовать object вместо string без возможного нарушения LSP в этом отношении (Вы можете и Base/Derived все еще не нарушать LSP, нарушение LSP скрыто внутри Derived и не видно снаружи).

Есть несколько действительно замечательных статей Эрика Липперта о ковариантности и контравариантности в C#, начиная с Covariance и Contravariance в C#, часть первая (доходит до части 11).

Больше можно найти здесь:
Ковариантность и Контравариантность

В качестве примечания: не нарушая LSP, за него стоит бороться, но это не всегда самый экономичный выбор. Работая со сторонними или устаревшими API, иногда просто нарушая LSP, можно сэкономить здравый смысл, время и ресурсы.

Упоминается, что противоречивость аргументов является частью LSP, но я не могу понять, почему и как это может работать. Т.е. в подклассе переопределяющий метод может принимать более широкий (менее производный аргумент).

Прежде всего, давайте удостоверимся, что мы определили наши условия.

"Ковариантность" - это свойство отношений и преобразований. В частности, это свойство, которое поддерживает определенное отношение в течение определенного преобразования. "Контравариантность" - это то же самое, что и ковариантность, за исключением того, что конкретное отношение сохраняется, но переворачивается при трансформации.

Давайте приведем пример. У меня есть тип в руке, и я хочу превратить его в другой тип по правилу T превращается в Func<T>, У меня есть отношение между типами: "выражение типа X может быть назначен переменной типа Y"Например, выражение типа Giraffe может быть назначен переменной типа Animal, Преобразование является ковариантным, потому что отношение сохраняется по всему преобразованию: выражение типа Func<Giraffe> может быть назначен переменной типа Func<Animal>,

Преобразование T превращается в Action<T> меняет отношение: Action<Animal> может быть назначен Action<Giraffe>,

Но T в Action<T> является формальным параметром типа метода, представленного делегатом. Итак, как вы видите, у нас может быть противоположность формальных типов параметров.

Что это значит для переопределения метода? Когда ты сказал

class B 
{
  public virtual void M(Giraffe g) { b body }
}
class D : B 
{
  public override void M(Giraffe g) { d body }
}

Это логически так же, как

class B 
{
  protected Action<Giraffe> a = g => { b body }; 
  public void M(Giraffe g) { this.a(g); }
}
class D : B 
{
  public D() {  
    this.a = g => { d body };
  }
}

Правильно? Для нас было бы совершенно законно заменить конструктор D на

   this.a = some Action<Animal>

право? Однако C# - и большинство других языков OO, но не все - не позволяют

class D : B 
{
  public override void M(Animal a) { d body }
}

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

Как это могло сработать? Я имею в виду, как я могу выполнить договор, если менее производный аргумент не обладает нужными мне свойствами?

Ну, если бы ты не мог, то ты бы не стал, не так ли?

Предположим, мне нужно

int CompareHeights(Giraffe g1, Giraffe g2)

Кажется ли это настолько неправдоподобным, что я мог бы заменить это методом

int CompareHeights(Animal a1, Animal a2)

? Мне нужен метод, который сравнивает высоты жирафов, у меня есть метод, который сравнивает высоты животных, так что я все сделал, верно?

Предположим, мне нужно

void Paint(Circle, Color)

Кажется ли это неправдоподобным, что я мог бы заменить это методом

void Paint(Shape, Color)

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

Если мне нужно

int GetLength(string)

и я имею

int GetLength(IEnumerable)

тогда я в порядке. Мне нужен метод, который получает длину строки, которая представляет собой последовательность символов. У меня есть метод, который может получить длину любой последовательности, так что я в порядке.

Это может сработать, потому что потребители базового класса вызывают его, используя интерфейс базового класса (им не нужно знать о производном классе), и поскольку подкласс является менее строгим, любой аргумент, который может быть передан в базовый класс Метод может быть передан в метод подкласса. (В вашем примере, потребитель Base пройдет string в Base.GetLenght (и он не мог передать любой другой тип, потому что это ссылка на метод с string параметр), который переопределяется Derived.GetLenght, а также Derived.GetLenght получит string аргумент, который действителен для этого).

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

Конечно, переопределяющий метод должен уметь обрабатывать свои аргументы. В этом примере это означает, что он должен иметь способ получения "длины" любого объекта и должен определять, что это на самом деле означает.

Семантика

Хорошо написанный базовый класс будет определять контракт того, что должен делать метод (например, в комментарии). Соответствие принципу подстановки Лискова в производном классе также требует семантического соответствия этому (по крайней мере, не противоречащего ему), чтобы он не возвращал неожиданные (но синтаксически допустимые) значения для потребителя базового класса.

В этом примере это, вероятно, будет означать (в зависимости от семантического контракта этого метода), что если аргумент был string, переопределяющий метод должен возвращать длину (требуется проверка его типа и приведение его).

Например, метод базового класса может иметь комментарий о том, что: returns the number of characters in the string and returns -1 if 's' is null.

Переопределяющий метод может иметь комментарий, который говорит Given any sequence or collection (including a string, which is treated as a sequence of characters for this purpose), this returns the number of elements in the sequence or collection, and returns -1 if the argument is null or not a collection or sequence. Обратите внимание, что по этому определению, он делает то же самое, что и базовый класс, когда передается строка. Если это не так (например, возвращая 0 или выбрасывая исключение при передаче null), это будет нарушением Лискова, даже если его тип параметра будет таким же.

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

Возможная реализация

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

Если один из методов имел параметр, который не был двоично-совместимым с другим (например, если базовый класс принял значение int (для которого компилятор передал только 32-разрядное значение), а производный класс принял объект), переопределение Метод (в исходном коде) будет скомпилирован как новый метод (со своей собственной записью в таблице виртуальных методов), и компилятор может внутренне сгенерировать метод с прототипом метода базового класса, который преобразует аргумент и передает его к основному методу. (Последнее - это то, на что будет указывать запись таблицы виртуальных методов в производном классе.)

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

Другие вопросы по тегам