Нюансы кастования в C#

от автора

Всем привет. Не так давно добавлял поддержку кастования через ‘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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *