![image](https://habrastorage.org/webt/fg/ef/t8/fgeft8rc4cv-bveq_kt7qbia91g.jpeg)
В первой части книги рассмотрены основы C#, включая объектно-ориентированное программирование и новые возможности C# 9, такие как создание экземпляров новых объектов с целевым типом и работа с неизменяемыми типами с использованием ключевого слова record. Во второй части рассматриваются API .NET для выполнения таких задач, как управление данными и запросы к ним, мониторинг и повышение производительности, а также работа с файловой системой, асинхронными потоками, сериализацией и шифрованием. В третьей части на примерах кросс-платформенных приложений вы сможете собрать и развернуть собственные: например, веб-приложения с использованием ASP.NET Core или мобильные приложения на Xamarin Forms.
К концу книги вы приобретете знания и навыки, необходимые для использования C# 9 и .NET 5 для разработки сервисов, веб- и мобильных приложений.
Реализация интерфейсов и наследование классов
Данная глава посвящена созданию новых типов на основе существующих с использованием объектно-ориентированного программирования (ООП). Вы узнаете о том, как определять операции и локальные функции для выполнения простых действий, как определять делегаты и события для обмена сообщениями между типами, как реализовывать интерфейсы для разработки общей функциональности, и о дженериках. Кроме того, узнаете о разнице между ссылочными типами и типами значений, о том, как создавать новые классы, наследуя их от базовых для многократного применения функциональности, как переопределять член типа, использовать полиморфизм, создавать методы расширения и выполнять приведение классов в иерархии наследования.
В этой главе:
- настройка библиотеки классов и консольного приложения;
- упрощение методов;
- вызов и обработка событий;
- реализация интерфейсов;
- оптимизация типов под многократное использование с помощью дженериков;
- управление памятью с помощью ссылочных типов и типов значений;
- наследование классов;
- приведение в иерархиях наследования;
- наследование и расширение типов .NET.
Настройка библиотеки классов и консольного приложения
Мы начнем с определения рабочей области с двумя проектами, подобного созданному в главе 5. Если вы выполнили все упражнения в той главе, то можете открыть созданную тогда рабочую область и продолжить работу.
В противном случае следуйте приведенным ниже инструкциям.
1. В существующей папке Code создайте папку Chapter06 с двумя подпапками, PacktLibrary и PeopleApp, как показано в следующей иерархии:
- Chapter06
— PacktLibrary
— PeopleApp
2. Запустите программу Visual Studio Code.
3. Выберите команду меню File ->Save Workspace As (Файл -> Сохранить рабочую область как), введите имя Chapter06 и нажмите кнопку Save (Сохранить).
4. Выберите команду меню File->Add Folder to Workspace (Файл->Добавить папку в рабочую область), выберите папку PacktLibrary и нажмите кнопку Add (Добавить).
5. Выберите команду меню File->Add Folder to Workspace (Файл->Добавить папку в рабочую область), выберите папку PeopleApp и нажмите кнопку Add (Добавить).
6. Выберите команду меню Terminal->New Terminal (Терминал->Новый терминал) и выберите PacktLibrary.
7. На панели TERMINAL (Терминал) введите следующую команду:
dotnet new classlib
8. Выберите команду меню Terminal->New Terminal (Терминал->Новый терминал) и выберите PeopleApp.
9. На панели TERMINAL (Терминал) введите следующую команду:
dotnet new console
10. На панели EXPLORER (Проводник) в проекте PacktLibrary переименуйте файл Class1.cs в Person.cs.
11. Измените содержимое файла:
using System;
namespace Packt.Shared
{
public class Person
{
}
}
12. На панели EXPLORER (Проводник) разверните папку PeopleApp и щелкните кнопкой мыши на файле PeopleApp.csproj.
13. Добавьте ссылку на проект в PacktLibrary:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference
Include="..\PacktLibrary\PacktLibrary.csproj" />
</ItemGroup>
</Project>
14. На панели TERMINAL (Терминал) для папки PeopleApp введите команду dotnet build и обратите внимание на выведенную информацию, указывающую, что оба проекта были успешно созданы.
15. Добавьте операторы в класс Person для определения трех полей и метода, как показано ниже:
using System;
using System.Collections.Generic;
using static System.Console;
namespace Packt.Shared
{
public class Person
{
// поля
public string Name;
public DateTime DateOfBirth;
public List<Person> Children = new List<Person>();
// методы
public void WriteToConsole()
{
WriteLine($"{Name} was born on a {DateOfBirth:dddd}.");
}
}
}
Упрощение методов
Сделаем так, чтобы два экземпляра человека произвели потомство. Мы можем реализовать это, написав методы. Методы экземпляра — действия, которые объект выполняет для себя; статические методы — действия, которые выполняет сам тип. Ваш выбор зависит от того, что больше всего подходит для действия.
Имеет смысл использовать статические методы и методы экземпляров для выполнения аналогичных действий. Например, тип string имеет как статический метод Compare, так и метод экземпляра CompareTo. Это позволяет программистам, применяющим ваш тип, выбрать, как задействовать функциональность, тем самым увеличивая гибкость.
Реализация функционала с помощью методов
Начнем с реализации некоторых функций, используя методы.
1. Добавьте один метод экземпляра и один статический метод в класс Person, который позволит двум объектам Person производить потомство:
// статический метод «размножения»
public static Person Procreate(Person p1, Person p2)
{
var baby = new Person
{
Name = $"Baby of {p1.Name} and {p2.Name}"
};
p1.Children.Add(baby);
p2.Children.Add(baby);
return baby;
}
// метод «размножения» экземпляра класса
public Person ProcreateWith(Person partner)
{
return Procreate(this, partner);
}
Обратите внимание на следующие моменты:
- в статическом методе static с именем Procreate объекты Person для создания передаются как параметры с именами p1 и p2;
- новый класс Person с именем baby создается с именем, составленным из комбинации двух человек, которые произвели потомство;
- объект baby добавляется в коллекцию Children обоих родителей, а затем возвращается. Классы — это ссылочные типы, то есть добавляется ссылка на объект baby, сохраненный в памяти, но не копия объекта;
- в методе экземпляра ProcreateWith объект Person, для которого производится потомство, передается в качестве параметра partner, и он вместе с this передается статическому методу Procreate для повторного использования реализации метода. Ключевое слово this ссылается на текущий экземпляр класса.
Метод, который создает новый объект или модифицирует существующий объект, должен возвращать ссылку на этот объект, чтобы вызывающая сторона могла видеть результаты.
2. В проекте PeopleApp в начале файла Program.cs импортируйте пространство имен для нашего класса и статически импортируйте тип Console, как показано ниже:
using System;
using Packt.Shared;
using static System.Console;
3. В методе Main создайте трех человек, у которых появятся дети, отметив, что для добавления символа двойной кавычки в строку необходимо поставить перед ним символ обратной косой черты с кавычкой, \":
var harry = new Person { Name = "Harry" };
var mary = new Person { Name = "Mary" };
var jill = new Person { Name = "Jill" };
// вызов метода экземпляра
var baby1 = mary.ProcreateWith(harry);
// вызов статического метода
var baby2 = Person.Procreate(harry, jill);
WriteLine($"{harry.Name} has {harry.Children.Count} children.");
WriteLine($"{mary.Name} has {mary.Children.Count} children.");
WriteLine($"{jill.Name} has {jill.Children.Count} children.");
WriteLine(
format: "{0}'s first child is named \"{1}\".",
arg0: harry.Name,
arg1: harry.Children[0].Name);
4. Запустите приложение и проанализируйте результат:
Harry has 2 children.
Mary has 1 children.
Jill has 1 children.
Harry's first child is named "Gary".
Реализация функционала с помощью операций
Класс System.String имеет статический метод Concat, который объединяет два строковых значения и возвращает результат:
string s1 = "Hello ";
string s2 = "World!";
string s3 = string.Concat(s1, s2);
WriteLine(s3); // => Hello World!
Вызов метода, подобного Concat, работает, но программисту может быть привычнее использовать символ + для сложения двух строковых значений:
string s1 = "Hello ";
string s2 = "World!";
string s3 = s1 + s2;
WriteLine(s3); // => Hello World!
Хорошо известная библейская фраза «Плодитесь и размножайтесь», то есть «производите потомство». Напишем код так, чтобы символ * (умножение) позволял создавать два объекта Person.
Мы делаем это путем определения статической операции для символа *. Синтаксис скорее похож на метод, поскольку, по сути, операция — это метод, но вместо имени метода используется символ, что сокращает синтаксис.
Символ * — лишь один из многих символов, которые можно использовать в качестве операции. Полный список символов приведен на сайте docs.microsoft.com/ru-ru/dotnet/csharp/programming-guide/statements-expressions-operators/overloadable-operators.
1. В проекте PacktLibrary в классе Person создайте статическую операцию для символа *:
// операция "размножения"
public static Person operator *(Person p1, Person p2)
{
return Person.Procreate(p1, p2);
}
В отличие от методов, операции не отображаются в списках IntelliSense для типа. Для каждой операции, которую вы определяете, также создайте метод, поскольку программисту может быть неочевидно, что операция доступна. Реализация операции может затем вызвать метод, повторно используя написанный вами код. Вторая причина предоставления метода заключается в том, что операции поддерживаются не каждым языковым компилятором.
2. В методе Main после вызова статического метода Procreate создайте еще один объект baby с помощью операции *:
// вызов статического метода
var baby2 = Person.Procreate(harry, jill);
// вызов операции
var baby3 = harry * mary;
3. Запустите приложение и проанализируйте результат:
Harry has 3 children.
Mary has 2 children.
Jill has 1 children.
Harry's first child is named "Gary".
Реализация функционала с помощью локальных функций
К новым возможностям языка C# 7.0 относятся локальные функции.
Это, по сути, перенос идеи локальных переменных на методы. Другими словами, локальными функциями являются методы, которые видимы и могут быть вызваны только в пределах метода, в котором были определены. В других языках программирования такие функции иногда называются вложенными или внутренними.
Локальные функции могут быть определены где угодно внутри метода: в начале, в конце или даже где-то посередине!
Мы воспользуемся локальной функцией для вычисления факториала.
1. В классе Person добавьте операторы, чтобы определить функцию Factorial, которая использует локальную функцию внутри себя для вычисления результата, как показано ниже:
// метод с локальной функцией
public int Factorial(int number)
{
if (number < 0)
{
throw new ArgumentException(
$"{nameof(number)} cannot be less than zero.");
}
return localFactorial(number);
int localFactorial(int localNumber) // локальная функция
{
if (localNumber < 1) return 1;
return localNumber * localFactorial(localNumber - 1);
}
}
2. В проекте Program.cs в методе Main добавьте оператор для вызова функции Factorial и запишите возвращаемое значение в консоль:
WriteLine($"5! is {harry.Factorial(5)}");
3. Запустите консольное приложение и проанализируйте результат:
5! is 120
Вызов и обработка событий
Методы часто описываются как действия, которые может выполнять объект. К примеру, класс List может добавить в себя элемент или очистить сам себя, а File может создавать или удалять файл в файловой системе.
События часто описываются как действия, которые происходят с объектом. Например, в пользовательском интерфейсе Button есть событие Click, определяющее, что происходит с кнопкой при щелчке на ней. События можно охарактеризовать еще и как способ обмена сообщениями между двумя объектами.
События построены на делегатах, поэтому начнем с рассмотрения того, как работают делегаты.
Вызов методов с помощью делегатов
Вы уже знакомы с наиболее распространенным способом вызова или выполнения метода, который заключается в синтаксисе с точкой для доступа к методу по его имени. Например, метод Console.WriteLine дает типу Console задание выводить сообщения с помощью метода WriteLine.
Другой способ вызова или выполнения метода заключается в использовании делегата. Если вы использовали языки, поддерживающие указатели на функции, то делегат можно представить, как типобезопасный указатель на метод.
Другими словами, делегат — это адрес метода в памяти, имеющего ту же сигнатуру, что и делегат, чтобы его можно было безопасно вызвать.
Для примера представьте, что в классе Person существует метод, который должен иметь тип string, передаваемый как единственный параметр, и возвращать int:
public int MethodIWantToCall(string input)
{
return input.Length; // неважно, что здесь выполняется
}
Я мог бы вызвать этот метод на экземпляре Person с именем p1 следующим образом:
int answer = p1.MethodIWantToCall("Frog");
Как вариант, я мог бы определить делегат с совпадающей сигнатурой для косвенного вызова метода. Обратите внимание: имена параметров могут не совпадать. Должны совпадать только типы параметров и возвращаемые значения, как показано в коде ниже:
delegate int DelegateWithMatchingSignature(string s);
Теперь я могу создать экземпляр делегата, указать его на метод и, наконец, вызвать делегат (который вызывает метод!):
// создание экземпляра делегата, который указывает на метод
var d = new DelegateWithMatchingSignature(p1.MethodIWantToCall);
// вызов делегата, который вызывает метод
int answer2 = d("Frog");
Вероятно, вы задались вопросом: «А в чем смысл?» Отвечу лаконично — в гибкости.
Например, мы могли бы использовать делегаты для создания очереди методов, которые нужно вызвать по порядку. Действия в очереди, которые необходимо выполнить, распространены в сервисах для обеспечения улучшенной масштабируемости.
Другой пример — разрешить параллельное выполнение нескольких действий. Делегаты имеют встроенную поддержку асинхронных операций, которые выполняются в другом потоке, что может обеспечить улучшенную скорость отклика. Вы узнаете, как это сделать, в главе 13.
Делегаты позволяют реализовывать события для отправки сообщений между различными объектами, которые не должны знать друг о друге.
Делегаты и события — одни из самых продвинутых функций языка C#, и обучение им может занять некоторое время, поэтому не беспокойтесь, если сразу не поняли принцип их работы!
Определение и обработка делегатов
Microsoft предоставляет два предопределенных делегата для использования в качестве событий. Они выглядят следующим образом:
public delegate void EventHandler(
object sender, EventArgs e);
public delegate void EventHandler<TEventArgs>(
object sender, TEventArgs e);
Если хотите определить событие в собственном типе, то вам следует использовать один из этих двух предопределенных делегатов.
1. Добавьте код, показанный ниже, в класс Person и обратите внимание на следующие моменты:
- код определяет поле делегата EventHandler с именем Shout;
- код определяет поле int для хранения AngerLevel;
- код определяет метод Poke;
- каждый раз, когда человека толкают (Poke), уровень его раздражения (AngerLevel) растет. Когда уровень раздражения достигает 3, поднимается событие Shout (возмущенный возглас), но только если делегат события указывает на метод, определенный где-либо еще в коде, то есть не null.
// событие
public event EventHandler Shout;
// поле
public int AngerLevel;
// метод
public void Poke()
{
AngerLevel++;
if (AngerLevel >= 3)
{
// если что-то слушает...
if (Shout != null)
{
// ...то вызывается делегат
Shout(this, EventArgs.Empty);
}
}
}
Проверка, является ли объект null до вызова одного из его методов, выполняется очень часто. Версия C# 6.0 и более поздние версии позволяют упростить проверки на null, вместив их в одну строку:
Shout?.Invoke(this, EventArgs.Empty);
2. В Program добавьте метод с соответствующей сигнатурой, который получает ссылку на объект Person из параметра sender и выводит некоторую информацию о них:
private static void Harry_Shout(object sender, EventArgs e)
{
Person p = (Person)sender;
WriteLine($"{p.Name} is this angry: {p.AngerLevel}.");
}
ObjectName_EventName — соглашение Microsoft для имен методов, которые обрабатывают события.
3. В методе Main добавьте оператор для назначения метода полю делегата, как показано ниже:
harry.Shout = Harry_Shout;
4. Добавьте операторы для вызова метода Poke четыре раза после назначения метода для события Shout:
harry.Shout = Harry_Shout;
harry.Poke();
harry.Poke();
harry.Poke();
harry.Poke();
Делегаты — многоадресные; это значит, вы можете назначить несколько делегатов одному полю делегата. Вместо оператора присваивания = мы могли бы использовать оператор +=, чтобы добавить больше методов к тому же полю делегата. При вызове делегата вызываются все назначенные методы, хотя вы не можете контролировать порядок их вызова.
5. Запустите приложение и проанализируйте результат, как показано в следующем выводе, и обратите внимание, что Гарри первое время только злится и начинает кричать после того, как его толкнут не менее трех раз:
Harry is this angry: 3.
Harry is this angry: 4.
Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — C#
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.