Что такое принцип подстановки Барбары Лисков
Что проверяют
Понимаете ли вы, что подстановка проверяется поведением, а не компилятором. C# тут особенно наглядный язык: массивы ковариантны, обобщённые интерфейсы бывают ковариантными и контравариантными, и все эти правила существуют ровно для того, чтобы подстановка не ломала корректность. Спрашивают, чувствуете ли вы разницу между «компилируется» и «работает как обещано».
Как отвечать
Правило: если S наследник T, любой код, написанный против T, обязан работать с S не хуже. Практически это четыре проверяемых требования к наследнику: не усиливать предусловия, не ослаблять постусловия, сохранять инварианты базового типа, не бросать исключений, которых база не обещала.
Классический пример нарушения:
class Rectangle {
public virtual int Width { get; set; }
public virtual int Height { get; set; }
}
class Square : Rectangle {
public override int Width {
set { base.Width = base.Height = value; }
}
}Код, который выставляет ширину и высоту по отдельности и ждёт понятную площадь, на квадрате получает другое число. По типам всё корректно, по смыслу нет.
Примеры из стандартной библиотеки тоже уместны: коллекция только для чтения, которая на Add бросает NotSupportedException, соблюдает интерфейс формально и нарушает контракт.
Где спотыкаются
Отвечают заученным примером про птицу, которая не летает, и не могут перевести его в правила про предусловия и постусловия. Без такого критерия любой спорный случай приходится решать интуицией.
Лечат нарушение проверкой типа. Ветка is Square внутри метода, принимающего Rectangle, не чинит иерархию, а фиксирует, что она неверна, и заодно ломает принцип открытости и закрытости.
Не помнят, что ковариантность массивов в C# это дыра именно в LSP: массив строк можно передать как массив object, и запись элемента другого типа падает уже в рантайме. Обобщённые интерфейсы с модификатором out закрывают дыру, разрешая ковариантность только там, где записи нет.