Всем привет. Не так давно добавлял поддержку кастования через '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);
В этих строчках кода и показан ответ на наш изначальный вопрос. Пользователь нашей библиотеки, коллеги по проекту или даже мы сами никогда не знаем, каким классом будет реализован конкретный интерфейс.
Теперь, полностью разобравшись в проблеме, я спокойно удаляю все проверки на этапе компиляции связанные с кастованиями через интерфейсы. Спасибо за внимание.
Автор: crackanddie