Наследование

Во третей лабе мы рассматривали возможности создания типов для математических объектов, а также всяких около-физических величин. Но которые, так или иначе, по сути были просто набором цифр с некоторой заданной семантикой.

Короче, тут мы будем делать тоже самое, потому что в программировании все так или иначе это набор цифр с семантикой.

Но в этой статье мы рассмотрим некоторые интересные возможности построения архитектуры приложения, которые позволят нам углубить семантику (то есть смысловую нагрузку) нашего типа или класса.

Будем эмулировать поведение торгового автомата.

У меня будет торговый автомат, который по нажатию на кнопки будет выдавать фрукты а именно:

  • Мандарины
  • Виноград
  • Арбузы

хотя википедия утверждает, что Виноград и Арбуз — это ягоды, и у нас нет основания ей не верить, мы в учебных целях будем считать их за фрукты.

Каждый из этих “фруктов” обладает набором характеристик, который может отличать его в ту или иную сторону от собратьев в его классе.

Есть много разных способов смоделировать мандарин, но я человек простой, поэтому буду рассматривать только следующие признаки:

  • Спелость (по шкале от 0% до 100%)
  • Количество долек
  • Наличие листика на фрукте (булево значение, true или false)

Виноград будет иметь следующие признаки

  • Спелость (по шкале от 0% до 100%)
  • Количество ягод на ветке
  • Черный или зеленый (заюзаем enum)

Арбуз будет иметь следующие признаки

  • Спелость (по шкале от 0% до 100%)
  • Количество косточек (кто-то ж их считает)
  • Наличие полосок (булево значение, true или false)

Создаем классы фруктов

Перво-наперво, создадим приложение Windows Forms.

Теперь под каждый фрукт создадим класс. Чтобы не захламлять сильно файлы кодом, создадим отдельный файлик под классы с фруктами и назовем его Fruits.cs

файлик получится пустой, поэтому сами загоняем в него болванку кода

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

// БУДТЕ ВНИМАТЕЛЬНЕЕ: ТУТ ДОЛЖЕН СТОЯТЬ ТОТ ЖЕ namespace что и в Program.cs
namespace WindowsFormsApp12
{

}

теперь давай добавим класс под мандарин. Получится:

namespace WindowsFormsApp12
{
    public class Mandarin
    {
        public int Ripeness = 0; // спелость
        public int SliceCount = 0; // количество долек
        public bool WithLeaf = false; // наличие листика
    }
}

ну, такой мандарин с нулями по умолчанию. Не забываем, что это пока только описание класса. Чтобы с ним можно было что-то делать надо насоздавать сначала экземпляров класса, примерно так:

var mandarin1 = new Mandarin{Ripeness=100, SliceCount=12, WithLeaf=true};
var mandarin2 = new Mandarin{Ripeness=75, SliceCount=15, WithLeaf=false};
...

то есть класс это типа основа, образ-прототип, то что Платон называл Идея (эйдос) любой вещи или существа — это самое глубокое, сокровенное и существенное в нем. Таким образом описание класса олицетворяет духовную природу объекта. А создавая экземпляр класса мы как бы реинкарниуруем его в мир материи.

Вот такая философия.

Так, чет я отвлекся, в общем, добавляем еще классы Винограда и Арбуза, получим:

namespace WindowsFormsApp12
{
    public class Mandarin
    {
        // ...
    }


    // виды винограда
    public enum GrapesType {black, green};
    // собственно сам виноград
    public class Grapes
    {
        public int Ripeness = 0;
        public int BerriesNumber = 0; // количество ягод
        public GrapesType type = GrapesType.black; // тип
    }

    // Арбуз
    public class Watermelon
    {
        public int Ripeness = 0;
        public int BonesNumber = 0; // количество косточек
        public bool HasStripes = false; // полосатость арбуза
    }
}

Моделируем автомат

Оценим сначала немного поведение автомата. У него есть емкость куда владелец аппарата засовывает фрукты, а потом приходит покупатель нажимает кнопку и получает фрукт который положили первым, потом приходит еще один покупатель, нажимает кнопку, получает фрукт, который положили вторым, и так до тех пор, пока фрукты в аппарате не кончатся.

Чтобы смоделировать поведение такого аппарата, надо будет решить следующие задачи:

  1. Так как у нас нет виртуального работника, который будет заполнять нам аппарат, нам надо будет придумать как хранить очередь с фруктами, очевидно нам понадобится некоторый список, и кнопка, по нажатию на которую будет этот список формироваться.
  2. Создать кнопку по клику, на которую, мы будем получать фрукт, который в текущей момент времени лежит в очереди первым
  3. Нам надо создать какое-нибудь поле, в которым мы будем отображать этот самый, выданный фрукт
  4. Ну и придумать еще какое-нибудь поле, чтобы информировать что фрукты кончились.

Создаем список фруктов

Как мы знаем объявление списка выглядит вот так:

var list = new List<ClassName>();

вместо ClassName можно подставить какой-нибудь класс, например, int или тот же наш Matrix или новый Mandarin.

Но как быть если нам хочется хранить сразу несколько типов? Мы очевидно не можем написать

var list = new List<Mandarin,Grapes,Watermelon>();

Студия на такое взбунтуется. И не даст скомпилироваться

Можно попробовать создать три списка,

var mandarins = new List<Mandarin>();
var grapse = new List<Grapes>();
var watermelons = new List<Watermelon>();

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

Строим иерархию классов

В общем как мы видим тут есть небольшая проблема. Но к счастью, для ее решения в C# существует механизм “Наследования”. Это не про то, когда кто-то наследует имущество или передает про наследство. Это про отношения родства, а-ля наследник. Причем ближе всего к этому иерархия типов в какой-нибудь биологии. Ну там есть всякие царства, в царствах там домены и так далее. Типа так:

Ну и получается, что каждый следующий уровень как-то уточняет или расширяет предыдущий уровень. Казалось бы, при чем здесь программирование, а очень даже причем. Большинство современных языков программирования позволяют определять иерархию классов, чтобы упростить код и сделать архитектуру приложения более естественной.

И так, мы хотим создать список фруктов и положить в него Мандарины, Виноград и Арбузы. Но не можем этого сделать, потому что список в любой момент времени может хранить объекты только одного типа. Зато можем создать класс Фрукт и сделать иерархию типов

Затем добавить список с типом Фрукты и подсовывать в него наши Мандарины, Винограды и Арбузы. Список будет их свободно хранить в себе, думая, что ему дают фрукты (что в принципе не далеко от правды).

Давайте это сделаем. Идем в файл Fruits.cs и добавляем класс Fruit:

namespace WindowsFormsApp12
{
    public class Fruit
    {
    }

    // ...
}

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

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

namespace WindowsFormsApp12
{
    public class Fruit {}

    public class Mandarin : Fruit // << ДОБАВИЛ Fruit
    {
        public int Ripeness = 0;
        public int SliceCount = 0;
        public bool WithLeaf = false;
    }

    public enum GrapesType { black, green };
    public class Grapes : Fruit // << И ТУТ Fruit
    {
        public int Ripeness = 0;
        public int BerriesNumber = 0;
        public GrapesType type = GrapesType.black;
    }

    public class Watermelon : Fruit // << И ЗДЕСЬ Fruit
    {
        public int Ripeness = 0;
        public int BonesNumber = 0;
        public bool HasStripes = false;
    }
}

Красота!

Строим интерфейс

Теперь сделаем интерфейс под наш аппарат, вот такой:

кнопки в принципе можно было бы и не переименовывать, но для красоты ничего не жалко.

Добавим теперь обработчик для заполнения нашего автомата, кликнем дважды на кнопку “Перезаполнить”, и попадем в класс формы. Что-то такое увидим:

namespace WindowsFormsApp12
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void btnRefill_Click(object sender, EventArgs e)
        {

        }
    }
}

сначала добавим поле под список фркутов:

namespace WindowsFormsApp12
{
    public partial class Form1 : Form
    {
        // ДОБАВИЛ, важный момент что указываем тип Fruit
        List<Fruit> fruitsList = new List<Fruit>();

        public Form1()
        {
            InitializeComponent();
        }

        private void btnRefill_Click(object sender, EventArgs e)
        {
            // тут пока ничего не пишу
        }
    }
}

Теперь подкрутим кнопку Перезаполнения. Для начала, попробуем заполнить его только мандаринами:

private void btnRefill_Click(object sender, EventArgs e)
{
    this.fruitsList.Clear();
    for(var i=0;i<10;++i)
    {
        // классно да, список типа Fruit, а кладем Mandarin
        // вот она: "сила наследования"
        this.fruitsList.Add(new Mandarin());
    }
}

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

public partial class Form1 : Form
{
    List<Fruit> fruitsList = new List<Fruit>();
    public Form1(){ /* ... */ }
    private void btnRefill_Click(object sender, EventArgs e) { /* ... */ }

    // функция выводит информацию о количестве фруктов на форму
    private void ShowInfo()
    {
        // заведем счетчики под каждый тип
        int mandarinsCount = 0;
        int grapesCount = 0;
        int watermellonsCount = 0;

        // пройдемся по всему списку
        foreach(var fruit in this.fruitsList)
        {
            // помните, что в списки у нас лежат фрукты,
            // то есть объекты типа Fruit
            // поэтому чтобы проверить какой именно фрукт
            // мы в данный момент обозреваем, мы используем ключевое слово is
            if (fruit is Mandarin) // читается почти как чистый инглиш, "если fruit есть Мандарин"
            {
                mandarinsCount += 1;
            }
            else if (fruit is Grapes)
            {
                grapesCount += 1;
            }
            else if (fruit is Watermelon)
            {
                watermellonsCount += 1;
            }
        }

        // а ну и вывести все это надо на форму
        txtInfo.Text = "Мндрн\tВнгрд\tАрбуз"; // буквы экнмлю, чтобы влезло на форму
        txtInfo.Text += "\n";
        txtInfo.Text += String.Format("{0}\t{1}\t{2}", mandarinsCount, grapesCount, watermellonsCount);
    }
}

и добавим вызов этой функции в конец обработчика заполнения и в конце конструктора формы, вот так:

public partial class Form1 : Form
{
    List<Fruit> fruitsList = new List<Fruit>();

    public Form1()
    {
        InitializeComponent();
        ShowInfo(); // СЮДА, чтобы при запуске приложения что-то показывалось на форме
    }

    private void btnRefill_Click(object sender, EventArgs e)
    {
        this.fruitsList.Clear();
        for(var i=0;i<10;++i)
        {
            this.fruitsList.Add(new Mandarin());
        }
        ShowInfo(); // И СЮДА
    }

    private void ShowInfo() { /*...*/ }
}

запускаем и проверяем:

Но все время десять мандаринов пихать, не ахти какое искусство, так что добавим немного рандома в нашу кнопку “Перезаполнить”.

private void btnRefill_Click(object sender, EventArgs e)
{
    this.fruitsList.Clear();
    var rnd = new Random();
    for(var i=0;i<10;++i)
    {
        switch(rnd.Next() % 3) // генерирую случайное число от 0 до 2 (ну остаток от деления на 3)
        {
            case 0: // если 0, то мандарин
                this.fruitsList.Add(new Mandarin());
                break;
            case 1: // если 1 то виноград
                this.fruitsList.Add(new Grapes());
                break;
            case 2: // если 2 то арбуз
                this.fruitsList.Add(new Watermelon());
                break;
            // появление других чисел маловероятно
        }
    }
    ShowInfo();
}

смотрим:

вот, уже интереснее.

Ну и конечно добавим реакцию на нажатие кнопки btnGet. Я хочу, чтобы было почти как в настоящем автомате, нажал на кнопку, количество товара в автомате уменьшилось, а в отверстии для изъятия товара появился товар.

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

И так, кликаем на кнопку “Взять” два раза и видим

public partial class Form1 : Form
{
    // ...

    private void btnGet_Click(object sender, EventArgs e)
    {
        // ПОЯВИЛАСЬ ФУНКЦИЯ
    }
}

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

private void btnGet_Click(object sender, EventArgs e)
{
    // если список пуст, то напишем что пусто и выйдем из функции
    if (this.fruitsList.Count == 0)
    {
        txtOut.Text = "Пусто Q_Q";
        return;
    }

    // взяли первый фрукт
    var fruit = this.fruitsList[0];
    // тут вам не реальность, взятие это на самом деле создание указателя на область в памяти
    // где хранится экземпляр класса, так что если хочешь удалить, делай это сам
    this.fruitsList.RemoveAt(0);

    // ну а теперь предложим покупателю его фрукт
    if (fruit is Mandarin)
    {
        txtOut.Text = "Манадрин";
    }
    else if (fruit is Grapes)
    {
        txtOut.Text = "Виноград";
    }
    else if (fruit is Watermelon)
    {
        txtOut.Text = "Арбуз";
    }

    // обновим информацию о количестве товара на форме
    ShowInfo();
}

проверяем:

В принципе у нас вполне себе рабочий автомат. И можно на этом закончить. Но нет же, мы пойдем дальше пилить архитектуру.

Упрощаем вывод информации о фруктах

Если взглянуть на код выше, особенно на кусок

if (fruit is Mandarin)
{
    txtOut.Text = "Манадрин";
}
else if (fruit is Grapes)
{
    txtOut.Text = "Виноград";
}
else if (fruit is Watermelon)
{
    txtOut.Text = "Арбуз";
}

мы можем подумать, а чего это мы должны каждый раз проверять что за фрукт выпал и в зависимости от этого выводить тот или иной текст, ведь в жизни нам нет необходимости проводить какой-нибудь особый анализ того что мы видим, чтобы понять, что оно есть. Как правило предмет прям кричит к(ч)то он есть.

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

Переключимся на файл Fruits.cs, и добавим в класс Fruit метод GetInfo(), который выдаст информацию о фрукте:


namespace WindowsFormsApp12
{
    public class Fruit
    {
        // добавил метод
        public String GetInfo()
        {
            return "Я фрукт";
        }
    }

    // ...
}

теперь давайте попробуем вернемся обратно к коду формы и поправим функцию btnGet_Click, так чтобы она вместо проверки типа фрукта, сразу выводила результат функции GetInfo на форму. Правим:

private void btnGet_Click(object sender, EventArgs e)
{
    if (this.fruitsList.Count == 0)
    {
        txtOut.Text = "Пусто Q_Q";
        return;
    }

    var fruit = this.fruitsList[0];
    this.fruitsList.RemoveAt(0);

    // ЗАМЕНИЛ НАШИ if`ы
    txtOut.Text = fruit.GetInfo();

    ShowInfo();
}

теперь давайте запустим и проверим что получилось:

Вообще в принципе все закономерно, мы вызываем функцию GetInfo класса Fruit, которая выдает строку “Я фрукт”, и выводим результат на форму. Казалось бы, ничего удивительного. Но на самом деле тут происходит магия полиморфизма и наследования.

Хочу напомнить, что в списке fruitsList у нас нет объектов типа Fruit, мы туда ложили мандарины, виноград и арбузы, и, если взглянуть на код классов Mandarin, Grapes, Watermelon, то никакого метода GetInfo в них нет.

Но за счет того чтобы наследовали эти классы от типа Fruit, все они неявно сделали метод GetInfo частью себя.

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

Переопределение

Но наша цель то была избавится от if`ов, и заменить их вызовом GetInfo, но хотелось бы, чтобы каждый фрукт выдавал информацию конкретно о себе.

В таких ситуациях нам приходит на помощь возможность переопределения родительской функции.

Для этого используется ключевое слово override. Переключимся на файл Fruits.cs и переопределим функцию GetInfo для мандарина, чтобы она выдавала информацию, собственно, о мандарине. Следующим образом:

namespace WindowsFormsApp12
{
    public class Fruit
    {
        // ТУТ ДОБАВИЛИ СЛОВО virtual ПОСЛЕ public
        // это надо чтобы функцию можно было переопределить
        // в классах наследниках
        public virtual String GetInfo()
        {
            return "Я фрукт";
        }
    }

    public class Mandarin : Fruit
    {
        public int Ripeness = 0;
        public int SliceCount = 0;
        public bool WithLeaf = false;

        // А ТУТ ПЕРЕОПРЕДЕЛИЛ МЕТОД
        public override String GetInfo()
        {
            return "Я мандарин";
        }
    }

    // ...
}

проверяем:

как видим для мандаринов, у нас подцепилась наша переопределенная функция, давайте теперь сделаем тоже самое для других фруктов:

namespace WindowsFormsApp12
{
    public class Fruit { /*...*/ }

    public class Mandarin : Fruit { /* ... */ }

    public enum GrapesType { black, green };
    public class Grapes : Fruit
    {
        public int Ripeness = 0;
        public int BerriesNumber = 0;
        public GrapesType type = GrapesType.black;

        // ДОБАВИЛ ПЕРЕОПРЕДЛЕНИЕ
        public override String GetInfo()
        {
            return "Я Виноград";
        }
    }

    public class Watermelon : Fruit
    {
        public int Ripeness = 0;
        public int BonesNumber = 0;
        public bool HasStripes = false;

        // ДОБАВИЛ ПЕРЕОПРЕДЛЕНИЕ
        public override String GetInfo()
        {
            return "Я Арбуз";
        }
    }
}

снова запускаем:

Обобщаем свойство

У нас у фруктов куча свойств, наверное, было бы здорово их все выводить. Давайте добавим информацию о спелости в информацию. Напомню, что спелость у нас в Ripeness.

Правим все обработчики:

public class Mandarin : Fruit
{
    public int Ripeness = 0;
    public int SliceCount = 0;
    public bool WithLeaf = false;

    public override String GetInfo() {
        // подправил
        var str = "Я мандарин";
        str += String.Format("\nСпелость{0}", this.Ripeness);
        return str;
    }
}

public class Grapes : Fruit
{
    public int Ripeness = 0;
    public int BerriesNumber = 0;
    public GrapesType type = GrapesType.black;

    public override String GetInfo()
    {
        // И ТУТ подправил
        var str = "Я Виноград";
        str += String.Format("\nСпелость{0}", this.Ripeness);
        return str;
    }
}

public class Watermelon : Fruit
{
    public int Ripeness = 0;
    public int BonesNumber = 0;
    public bool HasStripes = false;

    public override String GetInfo()
    {
        // И ЗДЕСЬ подправил
        var str = "Я Арбуз";
        str += String.Format("\nСпелость{0}", this.Ripeness);
        return str;
    }
}

если запустить, то ничего интересного:

давайте сделаем чтобы у нас этот Ripeness рандомно заполнялся, а то он все время равен нулю. Для этого нам надо слазить в обработчик кнопки “перезаполнить”

private void btnRefill_Click(object sender, EventArgs e)
{
    this.fruitsList.Clear();
    var rnd = new Random();
    for(var i=0;i<10;++i)
    {
        switch(rnd.Next() % 3) // генерирую случайное число от 0 до 2 (ну остаток от деления на 3)
        {
            case 0: // если 0, то мандарин
                this.fruitsList.Add(new Mandarin {
                    Ripeness = rnd.Next() % 101
                });
                break;
            case 1: // если 1 то виноград
                this.fruitsList.Add(new Grapes {
                    Ripeness = rnd.Next() % 101
                });
                break;
            case 2: // если 2 то арбуз
                this.fruitsList.Add(new Watermelon {
                    Ripeness = rnd.Next() % 101
                });
                break;
            // появление других чисел маловероятно
        }
    }
    ShowInfo();
}

C# по умолчанию создает конструктор особого вида, которые позволяет в момент создания экземпляра указать все его публичные поля (помеченные public). Т.е. когда мы создаем просто экземпляр класса мы пишем:

new Mandarin();

а если хотим указать значения публичного поля, то вместо круглых скобок используем фигурные, получается:

new Mandarin {
    Ripeness = rnd.Next() % 101
}

я использую операцию остатка от деления на 101, чтобы получать произвольное значение от 0 до 100.

Давайте еще раз запустим:

уже веселее!)

Теперь снова сходим в Fruits.cs и глянем на наши фрукты, если к ним присмотреться то у всех фруктов есть поле Ripeness.

Было бы здорово если бы это можно было как-нибудь обобщить. Для этого воспользуемся еще одним свойством наследования классов, которое позволяет выносить общие поля в основной класс.

Ripeness является основным свойством всех фруктов, поэтому добавим это поле в класс Fruit и удалим его из остальных классов:

public class Fruit
{
    public int Ripeness = 0; // ДОБАВИЛ, КАК ОБЩЕЕ ПОЛЕ ВСЕХ ФРУКТОВ

    public virtual String GetInfo()
    {
        return "Я фрукт";
    }
}

public class Mandarin : Fruit
{
    // УДАЛИЛ  СТРОЧКУ public int Ripeness = 0;
    public int SliceCount = 0;
    public bool WithLeaf = false;

    public override String GetInfo() { /*...*/ }
}

public enum GrapesType { black, green };
public class Grapes : Fruit
{
    // УДАЛИЛ  СТРОЧКУ public int Ripeness = 0;
    public int BerriesNumber = 0;
    public GrapesType type = GrapesType.black;

    public override String GetInfo() { /*...*/ }
}

public class Watermelon : Fruit
{
    // УДАЛИЛ  СТРОЧКУ public int Ripeness = 0;
    public int BonesNumber = 0;
    public bool HasStripes = false;

    public override String GetInfo() { /*...*/ }
}

Можно запустить и убедиться, что все работает как и раньше:

Вызов родительского метода

Еще раз взглянем на код и посмотрим можно ли тут еще что-нибудь упростить. Если глянуть, то видно, что во всех функциях GetInfo есть строчка

str += String.Format("\nСпелость: {0}", this.Ripeness);

так как Ripeness является базовым свойством фрукта, то возможно имеет смысл формирование этой строки утащить в функцию GetInfo класса Fruit. А для того чтобы получить сформированную строку в классах наследниках (Мандарины, Арбузы и т.д.) воспользоваться еще одной возможностью которую дает нас наследование – вызов функций базового класса.

Делается это с использованием ключевого слова base следующим образом

public class Fruit
{
    public int Ripeness = 0;

    public virtual String GetInfo()
    {
        // утащил сюда строчку с инфомацией о фрукте
        var str = String.Format("\nСпелость: {0}", this.Ripeness);
        return str;
    }
}

public class Mandarin : Fruit
{
    public int SliceCount = 0;
    public bool WithLeaf = false;

    public override String GetInfo() {
        var str = "Я мандарин";
        str += base.GetInfo(); // а тут заменил вызовом GetInfo базового класса, т.е. Fruit
        return str;
    }
}

public enum GrapesType { black, green };
public class Grapes : Fruit
{
    public int BerriesNumber = 0;
    public GrapesType type = GrapesType.black;

    public override String GetInfo()
    {
        var str = "Я Виноград";
        str += base.GetInfo(); // и тут так же
        return str;
    }
}

public class Watermelon : Fruit
{
    public int BonesNumber = 0;
    public bool HasStripes = false;

    public override String GetInfo()
    {
        var str = "Я Арбуз";
        str += base.GetInfo();  // и здесь
        return str;
    }
}

И снова если проверить, ничего не изменится.

Делаем рандомные фрукты

Давайте допишем наши функции GetInfo так чтобы они выдавали всю информацию по фрукту:

public class Mandarin : Fruit
{
    public int SliceCount = 0;
    public bool WithLeaf = false;

    public override String GetInfo() {
        var str = "Я мандарин";
        str += base.GetInfo();
        // Добавил
        str += String.Format("\nКоличество долек: {0}", this.SliceCount);
        str += String.Format("\nНаличие листика: {0}", this.WithLeaf);
        return str;
    }
}

public enum GrapesType { black, green };
public class Grapes : Fruit
{
    public int BerriesNumber = 0;
    public GrapesType type = GrapesType.black;

    public override String GetInfo()
    {
        var str = "Я Виноград";
        str += base.GetInfo();
        // Добавил
        str += String.Format("\nКоличество ягод: {0}", this.BerriesNumber);
        str += String.Format("\nТип: {0}", this.type);
        return str;
    }
}

public class Watermelon : Fruit
{
    public int BonesNumber = 0;
    public bool HasStripes = false;

    public override String GetInfo()
    {
        var str = "Я Арбуз";
        str += base.GetInfo();
        // Добавил
        str += String.Format("\nКоличество косточек: {0}", this.BonesNumber);
        str += String.Format("\nНаличие полосок: {0}", this.HasStripes);
        return str;
    }
}

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

То есть по идее нам надо сейчас поправить кнопку “Перезаполнить” чтобы в момент создания экземпляра классов рандомно заполнялись все свойства.

Но есть идее получше, взять и добавить к нашим фруктам статические методы генерации рандомного фрукта. По количеству строчек мы конечно ничего не выиграем, но архитектурно выиграем, так как генератор случайного мандарина будет прям в классе мандарина. Давайте добавим сначала для класса Mandarin, получится:

public class Mandarin : Fruit
{
    public int SliceCount = 0;
    public bool WithLeaf = false;

    public override String GetInfo() { /*...*/ }

    // добавили статический метод генерации случайного мандарина
    public static Mandarin Generate()
    {
        var rnd = new Random();
        return new Mandarin
        {
            Ripeness = rnd.Next() % 100, // спелость от 0 до 100
            SliceCount = 5 + rnd.Next() % 20, // количество долек от 5 до 25
            WithLeaf = rnd.Next() % 2 == 0 // наличие листика true или false
        };
    }
}

теперь подцепим этот метод в обработчике кнопки “Перезаполнить”:

private void btnRefill_Click(object sender, EventArgs e)
{
    this.fruitsList.Clear();
    var rnd = new Random();
    for(var i=0;i<10;++i)
    {
        switch(rnd.Next() % 3)
        {
            case 0:
                // всю работу по созданию экземпляра класса
                // берет на себя метод Generate
                // поэтому нам не нужно больше использовать new
                this.fruitsList.Add(Mandarin.Generate());
                break;
            case 1:
                // остально пока не трогаем
                this.fruitsList.Add(new Grapes {
                    Ripeness = rnd.Next() % 100
                });
                break;
            case 2:
                this.fruitsList.Add(new Watermelon {
                    Ripeness = rnd.Next() % 100
                });
                break;
        }
    }
    ShowInfo();
}

запускаем и проверяем:

возможно вы заметили, что хоть мы и используем генератор случайных чисел, у нас всё равно получается один и тот же набор параметров у мандаринов.

Связано это с тем что генератор случайных чисел генерирует числа на самом деле совсем не случайно. Он берет текущее время до каких-нибудь долей миллисекунд и от этого значения строит новое число по строгому алгоритму. Так как цикл for работает очень быстро то даже за доли секунд может произойти очень много всего, например, создать целую кучу якобы случайных мандаринов. Но так как у всех одно и тоже начальное значение для генерации случайных чисел, а мы этот генератор создаем каждый раз по новой и получаются одинаковые значения.

Короче, чтобы это поправить надо создать статический генератор случайных чисел, который создается один раз в начале работы программы и привязать его, например, к классу Fruit вот так:

public class Fruit
{
    // ДОБАВИЛ СТАТИЧЕСКОЕ ПОЛЕ rnd В БАЗОВЫЙ КЛАСС
    // вообще лучше написать protected вместо public, но и так сойдет
    public static Random rnd = new Random();

    //...
}

ну и, соответственно, поправить класс Мандарина

public class Mandarin : Fruit
{
    public int SliceCount = 0;
    public bool WithLeaf = false;

    // ...

    public static Mandarin Generate()
    {
        // ТУТ УБРАЛ СТРОЧКУ С rnd = new Random()
        // остальное не трогаем
        return new Mandarin
        {
            Ripeness = rnd.Next() % 100, // спелость от 0 до 100
            SliceCount = 5 + rnd.Next() % 20, // количество долек от 5 до 25
            WithLeaf = rnd.Next() % 2 == 0 // наличие листика true или false
        };
    }
}

запускаем еще раз:

теперь другое дело.

Давайте теперь еще добавим методы Generate для остальных фруктов:

public class Grapes : Fruit
{
    // ...

    public static Grapes Generate()
    {
        return new Grapes
        {
            Ripeness = rnd.Next() % 100, // спелость от 0 до 100
            BerriesNumber = 25 + rnd.Next() % 75, // количество ягод от 25 до 100
            type = (GrapesType)rnd.Next(2) // тип винограда
        };
    }
}

public class Watermelon : Fruit
{
    // ...

    public static Watermelon Generate()
    {
        return new Watermelon
        {
            Ripeness = rnd.Next() % 100, // спелость от 0 до 100
            BonesNumber = 250 + rnd.Next(250), // количество ягод от 250 до 500
            HasStripes = rnd.Next(2) == 0 // начличие полосок
        };
    }
}

и подцепим эти методы в кнопке “Перезаполнить”, получится такой код:

private void btnRefill_Click(object sender, EventArgs e)
{
    this.fruitsList.Clear();
    var rnd = new Random();
    for(var i=0;i<10;++i)
    {
        switch(rnd.Next() % 3)
        {
            case 0:
                this.fruitsList.Add(Mandarin.Generate());
                break;
            case 1:
                this.fruitsList.Add(Grapes.Generate());
                break;
            case 2:
                this.fruitsList.Add(Watermelon.Generate());
                break;
        }
    }
    ShowInfo();
}

Проверяем:

Красота! =)