Создание переменной типа <base class> для хранения объекта <производного классa> в С#

Я немного новичок в программировании, и у меня вопрос о классах, наследовании и полиморфизме в С#. Изучая эти темы, я иногда сталкиваюсь с кодом, который выглядит примерно так:

Animal fluffy = new Cat();  // where Animal is a superclass of Cat*

Это меня смущает, потому что я не понимаю, почему кто-то создавал переменную типа Animal для хранения объекта типа Cat. Почему бы просто не написать это человеку:

Cat fluffy = new Cat();

Я понимаю, почему это законно хранить дочерний объект в переменной родительского типа, но не почему это полезно. Есть ли веская причина хранить объект Cat в переменной Animal vs. a Cat? Может ли человек дать мне пример? Я уверен, что это имеет какое-то отношение к полиморфизму и переопределению метода (и/или скрытию метода), но я не могу обернуться вокруг него. Спасибо заранее!

Ответы

Ответ 1

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

 List<Animal> Animals = new List<Animal>();
 Animals.Add(new Cat());
 Animals.Add(new Dog());

Если вы когда-либо создавали проект с использованием Winforms, вы уже использовали бы что-то подобное, поскольку все элементы управления получают из Control. Затем вы заметите, что в окне есть список элементов управления (this.Controls), который позволяет вам одновременно получать доступ ко всем дочерним элементам управления. I.E, чтобы скрыть все элементы управления.

 foreach(var control in this.Controls)
      control.Hide();

Ответ 2

но не почему это полезно.

Взгляните на несколько лучших примеров:

Cat myCat = new Cat();
Dog myDog = new Dog();

List<Animal> zoo = ...;  // A list of Animal references
zoo.Add(myCat);          // implicit conversion of Cat reference to Animal reference
zoo.Add(myDog);

и

void CareFor(Animal animal) { ... }
CareFor(myCat);         // implicit conversion of Cat reference to Animal reference
CareFor(myDog);

Образец Animal fluffy = new Cat(); гораздо реже используется в реальном коде (но это происходит).

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

Ответ 3

Посмотрите на практический, но экстремальный пример.

class Animal { }
class Bird : Animal { }
class Cat : Animal { }
class Dog : Animal { }
class Elephant : Animal { }
class Fennec : Animal { }

Скажем, у нас есть класс Person. Как мы храним ссылку на его одинокое и уникальное домашнее животное?


Метод 1: безумный способ

class Person
{
    public Bird myBird;
    public Cat myCat;
    public Dog myDog;
    public Elephant myElephant;
    public Fennec myFennec;
}

В этом беспорядке, как мы получаем домашнее животное?

   if (myBird != null)
    {
        return myBird;
    }
    else if (myCat != null)
    {
        return myCat;
    }
    else if (myDog != null)
    {
        return myDog;
    }
    else if (myElephant != null)
    {
        return myElephant;
    }
    else if (myFennec != null)
    {
        return myFennec;
    }
    else
    {
        return null;
    }

И мне здесь приятно, всего 5 видов животных. Скажем, у нас более 1000 видов животных. Будете ли вы писать все эти переменные в классе Person и добавлять все эти "else if()" в любом месте в вашем приложении?


Метод 2: лучший подход

class Person
{
    public Animal myPet;
}

Таким образом, благодаря полиморфизму, у нас есть наша единственная и уникальная ссылка на человека-любимца, и, чтобы получить питомца, мы просто пишем:

return myPet;

Итак, что является лучшим способом вещей? Способ 1 или 2?

Ответ 4

Как еще не ответили, я постараюсь дать хороший ответ, насколько это возможно.

Взгляните на следующую программу:

class Program
{
    static void Main(string[] args)
    {
        Animal a = new Animal();
        Cat c = new Cat();
        Animal ac = new Cat();

        a.Noise(a);
        a.Noise(c);
        a.Noise(ac);

        c.Noise(a);
        c.Noise(c);
        c.Noise(ac);

        a.Poop();
        c.Poop();
        ac.Poop();

        Console.Read();

    }
}

public class Animal
{
    public void Noise(Animal a)
    {
        Console.WriteLine("Animal making noise!");
    }

    public void Poop()
    {
        Console.WriteLine("Animal pooping!");
    }
}

public class Cat : Animal
{
    public void Noise(Cat c)
    {
        Console.WriteLine("Cat making noise!");
    }

    public void Noise(Animal c)
    {
        Console.WriteLine("Animal making noise!");
    }

    public void Poop()
    {
        Console.WriteLine("Cat pooping in your shoe!");
    }
}

Вывод:

Animal making noise!
Animal making noise!
Animal making noise!

Animal making noise!
Cat making noise!
Animal making noise!

Animal pooping!
Cat pooping in your shoe!
Animal pooping!

Вы можете видеть, что мы создаем переменную a типа Animal. Он указывает на объект типа Animal. Он имеет статический и runtime-тип Animal.

Затем мы создаем переменную Cat, которая указывает на объект Cat. Третий объект - сложная часть. Мы создаем переменную Animal, которая имеет тип времени выполнения Cat, но статический тип Animal. Почему это важно? Потому что в компиляторе ваш компилятор знает, что переменная ac на самом деле имеет тип Animal. В этом нет сомнений. Таким образом, он сможет делать все, что может сделать объект Animal.

Однако во время выполнения объект внутри переменной известен как Cat.

Чтобы продемонстрировать, я создал 9 вызовов функций.

Сначала мы передаем объекты экземпляру Animal. Этот объект имеет метод, который принимает объекты Animal.

Это означает, что внутри Noise() мы можем использовать все методы и поля, которые имеет класс Animal. Ничего больше. Поэтому, если Cat будет иметь метод Miauw(), мы не смогли бы назвать его без литья нашего животного a Cat. (Типизация грязная, постарайтесь ее избежать). Поэтому, когда мы выполняем эти 3 вызова функций, мы будем печатать Animal making noise! три раза. Ясно. Итак, чем же тогда мой статический тип?

Ну, мы попадем через секунду.

Следующие три вызова функций - это методы внутри объекта Cat. Объект Cat имеет два метода Noise(). Один берет Animal, а другой принимает Cat.

Итак, сначала мы передаем ему обычный Animal. Среда выполнения рассмотрит все методы и увидит, что у нее есть метод Noise, который принимает Animal. Именно то, что нам нужно! Поэтому мы выполняем этот, и мы печатаем Animal, создавая шум.

Следующий вызов передает переменную Cat, которая содержит объект Cat. Опять же, время выполнения будет выглядеть. У нас есть метод, который принимает Cat, потому что это тип моей переменной. Да, да, мы делаем. Поэтому мы выполняем метод и печатаем "Cat making noise".

Третий вызов имеет переменную ac, которая имеет тип Animal, но указывает на объект типа Cat. Мы посмотрим, посмотрим, найдем ли мы способ, который бы соответствовал нашим потребностям. Мы рассмотрим статический тип (т.е. Тип переменной), и мы видим, что это тип Animal, поэтому мы вызываем метод, который имеет Animal в качестве параметра.

Это тонкая разница между ними.

Затем, pooping.

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

Вы заметите, что когда мы назовем Poop() на нашем Animal, мы получим ожидаемый результат. То же самое относится к Cat c. Однако, когда мы называем метод Poop на ac, мы видим, что он Animal pooping и ваш ботинок чист. Это связано с тем, что, опять же, компилятор сказал, что тип нашей переменной ac равен Animal, вы сказали так. Поэтому он будет вызывать метод в типе Animal.

Надеюсь, это достаточно ясно для вас.

Edit:

Я помню об этом, думая об этом так: Cat x; - это поле с типом Cat. Коробка не содержит кошку, однако она имеет тип Cat. Это означает, что поле имеет тип, независимо от его содержимого. Теперь, когда я храню в нем кот: x = new Cat();, я поместил внутри него объект типа Cat. Поэтому я поставил Кота в коробку для кошек. Однако, когда я создаю ящик Animal x;, я могу хранить животных в этом поле. Поэтому, когда я помещаю Cat внутри этого окна, это хорошо, потому что это животное. Итак, x = new Cat() хранит Cat внутри поля Animal, что хорошо.

Ответ 5

Объявление, которое включает инициализацию, например Animal joesPet = new Cat(), может иметь две цели:

  • Создайте идентификатор, который по всей своей области всегда будет представлять одно и то же.

  • Создайте переменную, которая изначально будет удерживать одну вещь, но позже может удерживать что-то еще.

Объявления, в которых инициализирована переменная родительского типа для ссылки на экземпляр подтипа, обычно используются для второй цели в ситуациях, когда переменная изначально назначается экземпляру определенного подтипа, но позже может потребоваться провести ссылки на вещи, которые не относятся к этому подтипу. Если декларация была Cat joesPet = new Cat(); или var joesPet = new Cat();, то было бы (к лучшему или худшему) не удастся сказать joesPet = new Dog();. Если код не сможет сказать joesPet = new Dog();, то тот факт, что объявление как Cat или var будет препятствовать тому, что это будет хорошо. С другой стороны, если код, возможно, должен иметь joesPet нечто иное, чем Cat, тогда он должен объявить переменную таким образом, чтобы это разрешить.

Ответ 6

Причина - полиморфизм.

Animal A = new Cat();
Animal B = new Dog();

Если Func принимает Animal и Animal реализует MakeNoise():

Func(A);
Func(B);


...

void Func(Animal a)
{
    a.MakeNoise();
}

Ответ 8

Я использую этот шаблон несколько раз, в несколько более продвинутом контексте, но, возможно, стоит упомянуть. При написании модульных тестов, выполняющих сервис/репозиторий или любой класс, реализующий интерфейс, я часто печатаю его переменную с интерфейсом вместо конкретного типа:

IRepository repository = new Repository();
repository.Something();
Assert.AreEquals(......);

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

Ответ 9

Если вы пишете программу, которая эмулирует поведение животных, все животные имеют общие черты. Они ходят, едят, дышат, уничтожают и т.д. Что они едят и как они ходят, между прочим, разные.

Итак, ваша программа знает, что все животные что-то делают, поэтому вы пишете базовый класс под названием Animal, который выполняет все эти вещи. То, что все животные делают одинаково (дыхание, исключение), вы можете программировать в базовом классе. Затем в подклассах вы пишете код, который обрабатывает все, что они делают, но не иначе, как другие животные, например, что они едят и как они ходят.

Но логика, которая контролирует поведение каждого животного, не заботится о деталях того, как они делают что-либо. "Мозг" животного просто знает, что время есть, ходить, дышать или устранять. Таким образом, он вызывает метод, который выполняет эти действия на переменной типа Animal, которая в конечном итоге вызывает правильный метод в зависимости от того, какой фактический тип Animal имеет объект, на который он ссылается.