Всем привет. Не так давно добавлял поддержку кастования через ‘as’ к себе в компилятор и задался вопросом — в каких случаях я получу Compile Time ошибку? Если заинтересовал — прошу под кат.
Решил начать с простого:
class Dog { } class Cat { } Dog dog = new Dog(); Cat cat = dog as Cat; // error: CS0039 Cannot convert type 'Dog' to 'Cat'
Тут, вроде, все логично: при кастовании смотрим — является ли тип кастуемого экземпляра дочерним от типа, к которому кастуем; или является ли тип кастуемого экземпляра родительским от типа, к которому кастуем. Если одно из условий верно, то ошибок во время компиляции возникать не должно.
Немного усложним ситуацию — добавим интерфейсы:
interface IBarkable { } class Dog : IBarkable { } interface IMeowable { } class Cat : IMeowable { } Dog dog = new Dog(); IMeowable cat = dog as IMeowable;
В этом случае никаких ошибок не возникает. Но почему? Не разобравшись в вопросе, я решил — «Да ладно, просто буду смотреть, если пользователь кастует экземпляр класса к интерфейсу — не будем ругаться». Добавил соответствующие проверки в компилятор и закоммитил.
Но где-то в глубине души я все еще задавался вопросом — «Почему же оно так, не может же быть все так просто?». Ведь, это действительно так странно, почему компилятор C# не выдает мне ошибку на этапе компиляции? Он же видит, что класс Dog никак не реализует интерфейс IMeowable.
Видимо Очевидно, мои проверки не были верны, так как следующий код:
Dog dog = new Dog(); IMeowable cat = dog as IMeowable; Dog dogAgain = cat as Dog; // тоже без ошибок компиляции
компилировался тоже без ошибок. И что это значит? Что мы любой класс можем кастовать к любому интерфейсу и наоборот? Но почему? Почему компилятор не предостерегает нас от этого?
Честное слово, я гуглил, гуглил достаточно. Возможно, по всем сайтам, которые я посетил, можно было бы и добыть всю нужную мне информацию. Но я не смог. Не удержался. Через пару минут ChatGpt уже пытался объяснить мне, почему так происходит. Примерный ответ по памяти:
Вооот, там тяжело проверить это все на этапе компиляции и т.д, и т.п.
Сидел и думал — либо я дурак, либо сани не едут я чего-то не понимаю в проверках наследования/имплементации. Ну, как так можно, не суметь проверить имплементации интерфейсов. Да, дольше, чем просто проверять наследование, но реализуемо! Или нет?
Тут меня осенило, я забыл про хитрую «фичу» C# — класс, который напрямую (либо через наследуемые типы) не реализует конкретный интерфейс, все еще может без проблем кастоваться к нему, но с «небольшим условием». И это условие заключается в следующем:
Представим, что мы написали такой прекрасный код и собрали его в библиотеку:
public class Dog { } public interface IMeowable { string SayMeow(); } public class Cat : IMeowable { public string SayMeow() { return "Cat says 'Meow'"; } } public class CoolClass { public static string DogMeows(Dog dog) { IMeowable meowable = dog as IMeowable; return meowable.SayMeow(); } }
Если бы я увидел такой код до написания этой статьи, я бы подумал — «А в чем суть метода DogMeows, если Dog не реализует интерфейс IMeowable?». Но теперь же прошу — вот ответ на этот вопрос:
Представим, что нашу библиотеку подключил странный конечный пользователь и написал такое:
class DogThatMeows : Dog, IMeowable { public string SayMeow() { return "Dog says 'Meow'"; } } var strangeDog = new DogThatMeows(); var result = CoolClass.DogMeows(strangeDog);
В этих строчках кода и показан ответ на наш изначальный вопрос. Пользователь нашей библиотеки, коллеги по проекту или даже мы сами никогда не знаем, каким классом будет реализован конкретный интерфейс.
Теперь, полностью разобравшись в проблеме, я спокойно удаляю все проверки на этапе компиляции связанные с кастованиями через интерфейсы. Спасибо за внимание.
ссылка на оригинал статьи https://habr.com/ru/articles/882888/
Добавить комментарий