Nano Hash - криптовалюты, майнинг, программирование

Почему принцип подстановки Лисков требует, чтобы аргумент был контравариантным?

Одно из правил, которое принцип подстановки Лисков накладывает на сигнатуру метода в производном классе:

Контравариантность аргументов метода в подтипе.

Если я правильно понял, это говорит о том, что функция переопределения производного класса должна разрешать контравариантные аргументы (аргументы супертипа). Но я не мог понять причину этого правила. Поскольку LSP говорит в основном о динамической привязке типов к их подтипам (а не супертипам) для достижения абстракции, поэтому разрешение супертипов в качестве аргументов метода в производном классе меня довольно сбивает с толку. Мои вопросы:

  • Почему LSP разрешает/требует контравариантных аргументов в функции переопределения производного класса?
  • Как правило контравариантности помогает в достижении абстракции данных/процедур?
  • Есть ли реальный пример, где нам нужно передать контравариантный параметр в переопределенный метод производного класса?

  • Они не обязательно должны быть контравариантными, они могут быть и просто инвариантными (имеющими один и тот же тип). 09.09.2019

Ответы:


1

Здесь, следуя тому, что говорит LSP, «производный объект» должен использоваться в качестве замены «базового объекта».

Допустим, у вашего базового объекта есть метод:

class BasicAdder
{
    Anything Add(Number x, Number y);
}

// example of usage
adder = new BasicAdder

// elsewhere
Anything res = adder.Add( integer1, float2 );

Здесь «Число» — это идея базового типа для числовых типов данных, целых чисел, чисел с плавающей запятой, двойных чисел и т. д. Такой вещи не существует, например, в C++, но мы не обсуждаем здесь конкретный язык. Точно так же, просто для примера, "Anything" отображает неограниченное значение любого типа.

Давайте рассмотрим производный объект, который «специализирован» на использовании комплекса:

class ComplexAdder
{
    Complex Add(Complex x, Complex y);
}

// example of usage
adder = new ComplexAdder

// elsewhere
Anything res = adder.Add( integer1, float2 ); // FAIL

следовательно, мы только что сломали LSP: его НЕ можно использовать в качестве замены исходного объекта, потому что он не может принимать integer1, float2 параметров, потому что на самом деле он требует сложных параметров.

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

Теперь рассмотрим другой случай:

class SupersetComplexAdder
{
    Anything Add(ComplexOrNumberOrShoes x, ComplexOrNumberOrShoes y);
}

// example of usage
adder = new SupersetComplexAdder

// elsewhere
Anything res = adder.Add( integer1, float2 ); // WIN

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

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

Также стоит отметить, что вы можете придерживаться или нарушать LSP на разных «уровнях».

class SmartAdder
{
    Anything Add(Anything x, Anything y)
    {
        if(x is not really Complex) throw error;
        if(y is not really Complex) throw error;

        return complex-add(x,y)
    }
}

Это, безусловно, похоже на соответствие LSP на уровне сигнатуры класса/метода. Но так ли это? Часто нет, но это зависит от многих вещей.

Как правило контравариантности помогает в достижении абстракции данных/процедур?

это хорошо.. очевидно для меня. Если вы создаете, скажем, компоненты, которые предназначены для замены/замены/замены:

  • БАЗА: вычислить сумму счетов наивно
  • DER-1: вычислить сумму счетов-фактур на нескольких ядрах параллельно
  • DER-2: вычислить сумму счетов с подробным ведением журнала

а затем добавить новый:

  • вычислить сумму счетов в другой валюте

и скажем, он обрабатывает входные значения EUR и GBP. Как насчет ввода в старой валюте, скажем, в долларах США? Если вы опустите это, то новый компонент не будет заменой старых. Вы не можете просто вынуть старый компонент и подключить новый и надеяться, что все в порядке. Все остальные вещи в системе могут по-прежнему отправлять значения в долларах США в качестве входных данных.

Если мы создадим новый компонент как производный от BASE, то каждый должен быть уверен, что может использовать его везде, где ранее требовалась BASE. Если в каком-то месте требовалось БАЗОВОЕ, а использовалось ДЕР-2, то мы должны иметь возможность подключить туда новый компонент. Это ЛСП. Если не можем, значит что-то сломалось:

  • любое место использования не требовало только BASE, но на самом деле требовало большего
  • или наш компонент действительно не является БАЗОЙ (обратите внимание на формулировку is-a)

Теперь, если ничего не сломано, мы можем взять одно и заменить другим, независимо от того, есть ли в наличии доллары США или фунты стерлингов, одноядерные или многоядерные. Теперь, глядя на общую картину на один уровень выше, если больше не нужно заботиться о конкретных типах валюты, то мы успешно абстрагируем ее, общая картина будет проще, в то время как, конечно, компоненты должны будут внутренне обрабатывать это. как-то.

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

Если компонент, полученный из BASE, не соответствует LSP, то он может вызывать ошибки при поступлении допустимых значений в долларах США. Или, что еще хуже, он не заметит и обработает их как GBP. У нас есть проблемы. Чтобы исправить это, нам нужно либо исправить новый компонент (чтобы соответствовать всем требованиям BASE), либо изменить другие соседние компоненты, чтобы они следовали новым правилам, таким как «теперь используйте EUR, а не USD, или сумматор будет генерировать исключения», или нам нужно добавьте элементы в общую картину, чтобы обойти ее, т. е. добавьте несколько ветвей, которые будут обнаруживать данные в старом стиле и перенаправлять их на старые компоненты. Мы просто «слили» сложность соседям (и, возможно, заставили их сломать SRP) или усложнили «общую картину» (больше адаптеров, условий, веток, ..).

31.10.2016
  • Спасибо за подробное описание. Однако Anything res = adder.Add( integer1, float2 ); // WIN это может быть правдой, если метод Add имеет Number в качестве аргумента в классе SupersetComplexAdder. Принимая во внимание тот факт, что BasicAdder ясно указывает, что он не ожидает ничего, кроме типа Number или его подтипа, в качестве аргумента в методе Add, предоставление супертипа в качестве аргумента в производном классе не дает никаких дополнительных возможностей вызывающей стороне. 31.10.2016
  • Даже если мы разрешим подтипу иметь ковариантные аргументы, в этом случае код вызывающей стороны (клиент) потеряет возможность замены объекта «SupersetComplexAdder» любым другим подтипом BasicAdder, поскольку теперь код более специфичен для SupersetComplexAdder. И это само по себе нарушает LSP для BasicAdder. Хотя я согласен с тем, что LSP по-прежнему верен для SupersetComplexAdder. 31.10.2016
  • Перечитав ваши ответы, я получил довольно четкое представление о важности поддержки ковариации в LSP :). Еще раз спасибо. 06.11.2016

  • 2

    Фраза «контравариантность аргументов метода» может быть краткой, но неоднозначной. Давайте использовать это в качестве примера:

    class Base {
      abstract void add(Banana b);
    }
    
    class Derived {
      abstract void add(Xxx? x);
    }
    

    Теперь «контравариантность аргумента метода» может означать, что Derived.add должен принимать любой объект, который имеет тип Banana или супертип, что-то вроде ? super Banana. Это неверная интерпретация правила LSP.

    Фактическая интерпретация такова: «Derived.add должен быть объявлен либо с типом Banana, как в Base, либо с каким-то надтипом Banana, например Fruit». Какой супертип вы выберете, зависит от вас.

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

    На практике не так много примеров, когда такое расширение типа в подклассе полезно. Я предполагаю, что именно поэтому большинство языков не заботятся о его реализации. Требование строго одного и того же типа также сохраняет LSP, но не дает вам полной гибкости, которую вы могли бы иметь при достижении LSP.

    31.10.2016
  • Спасибо за ваш ценный вклад. Но я по-прежнему настаиваю на своем вопросе: если базовый класс сам по себе не беспокоится о супертипе аргумента, который он имеет в своем переопределяющем методе, то это явный признак того, что базовый класс хочет, чтобы его клиенты (вызывающие ) для работы только с типом/подтипом аргументов. В таких случаях предоставление супертипа в качестве аргумента в производном классе не даст никаких новых условий вызывающим объектам, которые вызывают эти методы для объекта производного класса со ссылкой на суперкласс. 31.10.2016
  • Например, вызывающий абонент всегда будет звонить base.callMe(Number num); или base.callMe(Integer int) ;, но никогда base.callMe(Object obj);. 31.10.2016
  • Так же, как и ковариантные возвращаемые типы, это обслуживает только клиентов подтипа. 31.10.2016
  • Но дело в том, что в этом случае этот вызывающий код теряет возможность заменить объект любым другим подтипом базового класса, поскольку теперь он более специфичен для этого конкретного подтипа с ковариантным аргументом.! 31.10.2016
  • Однако это не нарушает LSP. Вы изменили код для работы с несвязанным типом в другой ветви иерархии. 31.10.2016
  • Но это нарушает LSP для базового класса. Правильно? 31.10.2016
  • Почему? Как? Не думаю, что я больше слежу за тобой. У нас был тип Base и тип Derived. Затем вы сказали, давайте представим некоторый код, написанный для типа Derived. Затем вы сказали, что мы не можем передать ему экземпляр несвязанного Derived2, что одновременно верно и не является нарушением LSP. Наконец, вы сказали, что это нарушает LSP для базового класса, но на этой картинке больше нет Base. Код написан против Derived. 31.10.2016
  • Мои извинения за путаницу. Похоже, это синий эффект понедельника на меня :). Во время написания моего последнего сообщения я думал о базовом классе. Но это было не так, я сам упоминал, что клиентский код написан специально для класса Derived и послушание LSP начнется с этой начальной точки в иерархии наследования. Еще раз спасибо за четкое объяснение. 31.10.2016

  • 3

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

     class BasicTester
        {
           TestDrive(Car f)
    
        }
    
        class ExpensiveTester:BasicTester
        {
           TestDrive(Vehicle v)
        }
    

    Старый класс может работать только с типом Car, тогда как производный класс лучше и может работать с любым Vehicle. Кроме того, будут обслуживаться и те, кто использует новый класс со «старым» типом автомобиля.

    Однако вы не можете переопределить это в C#. Вы можете реализовать это косвенно, используя делегаты:

    protected delegate void TestDrive(Car c)
    

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

    15.10.2018
    Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

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

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

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

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..