Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие
.pdf
Если начать увеличивать размерность массива a, легко убедиться, что пер-
вым способом время растет как квадрат от размера массива, а вторым и
третьим – почти линейно. При этом третий вариант примерно на 20 %
быстрее.
7.2. Чтение данных
Еще одной проблемой программистов, переходящих на C# с других
языков, является проблема чтения данных. В языке нет функций чтения
текстовых данных, читающих что-либо, кроме строк. Разбор строк и преобразование в другие типы делается независимо от чтения. Например, чтение двух целых чисел с консоли может быть выполнено следующим способом:
var strings = Console.ReadLine()?.Split([' ', ',', '\t', ';']);
if(strings?.Length != 2) throw new Exception("Неверное число параметров");
var N = int.Parse(strings[0]);
if (!int.TryParse(strings[1], out var M)) throw new Exception("Неверный параметр");
Функция Split разбивает строку по заданному массиву разделителей и
возвращает массив строк (в приведенном примере разделителями целых
чисел могут быть пробел, запятая, табуляция, точка с запятой).
Для преобразования строки в заданный тип данных все основные
типы реализуют статическую функцию Parse (мы использовали простейший ее вариант, но существуют и варианты с параметрами, определяющими способ преобразования). Эта функция пытается преобразовать
строку в нужный тип, а при невозможности этого выбрасывает какое-нибудь исключение (в зависимости от возникшей проблемы). Существует
также функция TryParse, которая возвращает True, если преобразование
прошло удачно, и False, если была ошибка.
Заметим, что приведенный пример является образцом не слишком хорошего стиля программирования. Обычно лучше читать параметры однотипным образом, т. е. либо оба раза использовать Parse, либо оба раза –
TryParse.
51

8. КОРТЕЖИ
Кортеж – это упрощенная структура для группировки нескольких элементов данных. Формально кортеж является типом-значением, у которого
все поля являются публичными. У кортежа нельзя определить никакие
свойства, функции и прочее, но зато синтаксис его определения максимально прост. Например, фрагмент программы
var t = (1, 2.0, 3);
создает экземпляр кортежа с полями int Item1, double Item2, int Item3. Есте-
ственно, справа могут стоять не только константы, но и любые переменные и даже выражения. Если имена полей по умолчанию не нравятся, их
можно задать явно, например:
var t = (First:1, Second:2.0, Third: 3);
(int First,double Second,double Third) t = ( 1, 2.0, 3);
Для кортежей определены операторы сравнения == и !=, а также оператор присваивания. Все эти операторы обрабатывают поля в порядке расположения и игнорируют имена полей.
Оператор присваивания = можно также использовать для деконструк-
ции экземпляра кортежа в отдельные переменные, например:
int a , b;
(a, b) = (10, 20);
Вообще, способов использования кортежей очень много. Например, в
C# нет аналога функции std::swap из С++, которая обменивает значениями
две переменные, но зато можно использовать деконструкцию с аналогичной целью:
(a, b) = (b, a);
Еще один рекомендуемый способ использования кортежей – возврат
нескольких значений из функций вместо out-параметров.
Заметим, что операция деконструкции при описании класса или
структуры может быть определена явно. Например, если у класса Person
определить функцию с именем Deconstruct
52

class Person
{
public string Name { get; set; }
public virtual string Position { get; init; }
public void Deconstruct(out string name, out string position)
{
name=Name; position=Position;
}
}
то следующий фрагмент программы
var person = new Person() { Name = "Федор", Position = "Профессор" };
var (a, b) = person;
Console.WriteLine(a + " " + b);
напечатает «Федор Профессор».

9. НАСЛЕДОВАНИЕ
Наследование – это первое, что отличает структуры данных при объектно ориентированном подходе. Наследование в C# позволяет определить некоторый базовый тип-класс, а затем определить порожденные
классы, которые имеют все свойства базового класса, но при этом могут
иметь дополнительные возможности и переопределять какие-то функции
базового класса. Перед тем как разбирать синтаксис наследования, попробуем привести простой пример.
Пусть базовый класс Person предназначен для хранения информации
о людях. Допустим, мы хотим хранить информацию о преподавателях и
студентах университета. Тогда можно унаследовать от этого класса два
других класса, Teacher и Student, которые будут изменять функциональность базового:
class Person
{
public string Name { get; init; }
public virtual string Position { get; init; }
}
class Teacher:Person
{
public Teacher(string position) { Position = position;}
}
class Student:Person
{
public override string Position => "student";
}
У класса Person определены два свойства: имя (Name) и должность
(Position). Ключевое слово virtual говорит, что порожденные классы могут
переопределить это свойство. В приведенном примере класс Student переопределяет это свойство константой. Ключевое слово override в С# является обязательным при переопределении функции или свойства, оно показывает компилятору, что программист не просто так дал функции то же
самое имя (в отличие то С++, где override не является обязательным, и достаточно часто встречаются ошибочные переопределения). Заметим, что
существует еще ключевое слово new, позволяющее определить у порожденного класса функцию с тем же именем, но не переопределяющую виртуальную функцию. В этом случае при преобразовании типа к базовому
будет вызываться функция базового класса, а не наследника.
54

Наследование означает, что порожденный тип может быть подставлен
в алгоритм в качестве базового. Например:
public static void PrintInfo(Person person)
{
Console.WriteLine("Name: " + person.Name + " Position:" + person.Position);
}
static void Main()
{
var person = new Teacher("Professor");
PrintInfo(person);
}
9.1. Базовый класс object
В C# практически все типы (кроме, например, ref struct) неявно наследуются от класса object. У этого базового класса есть три виртуальные
функции, которые любой класс может переопределить: bool Equals(object
other) – функция сравнения с другим объектом, int GetHashCode() – возвращает хэш-код экземпляра класса, string ToString() – возвращает строковое представление объекта. Все эти функции по умолчанию имеют какуюто реализацию. ToString для многих определенных программистом типов
по умолчанию возвращает имя типа, GetHashCode для классов по умолчанию создает хэш-код на основе ссылки на объект и не учитывает его содержимое, Equals для ссылочных типов проверяет равенство ссылок, а для
типов-значений сравнивает содержимое полей. Отметим, что переопределять Equals и GetHashCode следует одновременно, поскольку равные объекты обязаны возвращать равные хэши.
Кроме того, у object есть непереопределяемая функция GetType, которая возвращает полную информацию о фактическом типе экземпляра.
9.2. Тип record
Начиная с 9-й версии языка, в C# появился тип record. Этот тип придумали, чтобы немного упростить жизнь программистам. Record – это ссы-
лочный тип, поведение которого похоже на тип-значение. Как и class, он
может наследоваться, но только от другого record. В принципе, описатель
record – сокращение от record class, поэтому тип record может быть описан
традиционным способом. Но существует и сокращенный способ описания:
record Person(string Name, string Position);
55

Приведенная строка определяет класс, содержащий свойства Name и
Position с доступом на чтение и инициализацию, имеющий конструктор с
двумя строковыми параметрами. У этого класса определена деконструкция для двух строковых переменных, переопределены операторы == и !=
так, чтобы сравнение выполнялось не по ссылкам, а по содержимому
(две записи считаются равными, если они имеют одинаковый тип и хранят
одинаковые значения), аналогично переопределены функции Equals и
GetHashCode. Кроме того, переопределена функция ToString так, чтобы
получалась форматированная строка с именами свойств и их значениями.
Например, фрагмент кода
var p = new Person("Федор", "Профессор");
Console.WriteLine(p);
напечатает
Person { Name = Федор, Position = Профессор }
Для типа record возможно смешение сокращенного описания и тради-
ционного. Например,
record Person(string FirstName, string SecondName)
{
public virtual string Position { get; init; }
}
описывает запись с тремя публичными полями FirstName, SecondName и
Position и конструктором с двумя параметрами. Заметим, что деконструк-
ция автоматически определяется с теми же параметрами, что и конструктор по умолчанию (т. е. в данном случае для двух строковых переменных),
а вот ToString печатает все публичные свойства, независимо от способа их
описания.
Поскольку record – ссылочный тип, оператор присваивания копирует
ссылку на объект, а не создает новый объект с копией содержимого.
Но для типа record определен специальный конструктор копирования с
ключевым словом with, который позволяет определять новый объект из
старого через присваивание с возможным изменением содержимого.
Например, программа
var p = new Person("Федор", "Профессор") ;
var c = p with { Position = "Доцент" };
Console.WriteLine(p);
56

Console.WriteLine(c);
record Person(string Name, string Position);
напечатает
Person { Name = Федор, Position = Профессор }
Person { Name = Федор, Position = Доцент }
9.3. Тип record struct
Начиная с 10-й версии языка, в C# добавились типы record struct и
readonly record struct. Отличие от record class состоит в первую очередь в
том, что это типы-значения, а не ссылочные типы. Поэтому для этих типов
невозможно наследование, но остальные возможности (краткий синтаксис
определения, деконструкция, with и т. п.) работают аналогично record
class. При этом свойства, создаваемые по упрощенному синтаксису, у
readonly record struct, как и у record class, имеют модификаторы get и init,
а у record struct – модификаторы get и set.
9.4. Создание экземпляра класса при наследовании
При наследовании инициализация экземпляра происходит в следующем порядке:
1) все поля экземпляра получают значения по умолчанию (ссылки при-
нимают значения null, числовые поля – ноль и т. д.);
2) выполняются инициализаторы полей в последнем производном
типе;
3) выполняются инициализаторы полей в базовых типах в порядке
наследования от непосредственного родителя до object;
4) выполняются конструкторы экземпляров в порядке наследования,
начиная от object до непосредственного родителя;
5) выполняется конструктор экземпляра создаваемого класса.
Заметим, что статические конструкторы родительских классов, как
всегда, выполняются перед первым вызовом конструктора этого класса
(т. е. фактически начиная с самого базового класса).

10. ИНТЕРФЕЙСЫ
10.1. Идея и синтаксические особенности
В языке С++ и некоторых других класс может наследоваться от нескольких базовых классов. Такой подход называется множественным
наследованием. Он имеет очевидные преимущества: для добавления новых свойств класса достаточно добавить ему родителя. Однако множественное наследование создает дополнительные проблемы программисту:
например, при множественном наследовании не ясен порядок вызова конструкторов базовых классов, сложное поведение, если у базовых классов
есть общие предки, и т. д. В C# множественное наследование отсутствует,
у каждого класса может быть только один родитель. Но в каком-то смысле
компромиссом являются интерфейсы.
Интерфейс – это набор контрактов, которые должен выполнить любой
реализующий тип. Синтаксически объявление интерфейса очень похоже
на объявление класса (подчеркнуты необязательные части описания): <мо-
дификаторы интерфейса> interface <идентификатор> <параметры для
универсального интерфейса> : <базовый интерфейс> <требования к типам-параметрам> { <Тело интерфейса> };
Тело интерфейса может содержать без реализации методы, свойства,
индексаторы и события. В этом случае класс, реализующий интерфейс,
обязан их реализовать. При этом класс никак не ограничен в реализации
дополнительных свойств и методов. Например, если интерфейс требует
реализации свойства с доступом get, то реализующему классу не запрещается добавить set.
Интерфейс может содержать члены с реализацией по умолчанию: константы, операторы, статический конструктор, статические поля, методы,
свойства, индексаторы и события, объявления членов с использованием
явного синтаксиса реализации интерфейса. При этом интерфейс не может
содержать нестатические поля.
Обратим внимание, что реализации по умолчанию не означают, что
соответствующие свойства и методы автоматически появятся у класса, реализующего интерфейс. Они означают отсутствие необходимости реализовывать эти свойства и методы, но для вызова их будет необходимо приводить экземпляр к типу интерфейса.
58

Например:
interface IPerson
{
string Name { get; }
string Position { get; }
string PrintInfo => $"Name: {Name} Position: {Position}";
}
record Teacher (string Name, string Position, string Faculty) : IPerson;
record Student (string Name, string Position, string Faculty) : IPerson
{
public string PrintInfo
=> $"Name: {Name} Position: {Position} Faculty: {Faculty}";
}
class Test
{
static void Main()
{
var teacher = new Teacher("Федор" ,"Профессор", "ФПМИ") ;
// Console.WriteLine(teacher.PrintInfo);
// синтаксическая ошибка. Такого свойства нет.
IPerson person = teacher;
Console.WriteLine(person.PrintInfo); // А вот так можно.
var student = new Student("Сергей", "1 курс", "ФПМИ");
Console.WriteLine(student.PrintInfo); // определение свойства у класса есть
person = student;
Console.WriteLine(person.PrintInfo); // переопределилось свойство!
}
}
Заметим, что реализация по умолчанию в интерфейсах предназначена,
скорее, для расширения существующих интерфейсов в библиотеках,
чтобы не заставлять пользователей этих библиотек определять новые
функции для уже разработанных ими классов, реализующих эти интерфейсы, поэтому не стоит слишком увлекаться реализациями по умолчанию. Реализации, общие для наследников, лучше делать в абстрактных
классах.
В результате выполнения этого примера будет напечатан текст
Name: Федор Position: Профессор
Name: Сергей Position: 1 курс Faculty: ФПМИ
Name: Сергей Position: 1 курс Faculty: ФПМИ
В отличие от объявления класса, при объявлении интерфейса члены
без явно заданных модификаторов считаются публичными. Однако любые
59

модификаторы могут быть заданы явно (правда, если явно задать модификатор private, то необходимо задать и реализацию).
Начиная с 11-й версии C#, интерфейс может содержать абстрактные и
виртуальные статические члены, позволяющие требовать от класса,
например, переопределения операторов, что синтаксически было невозможно ранее. Подробное описание возможностей и синтаксиса такого переопределения можно найти в описании языка (например, https://learn.mi-
crosoft.com/ru-ru/dotnet/csharp/whats-new/tutorials/static-virtual-interface-members).
Заметим, что при реализации интерфейса класс может скрыть некоторые функции интерфейса, реализовав их явно, с указанием интерфейса,
например:
record Student (string Name, string Position, string Faculty) : IPerson
{
string IPerson.PrintInfo => "Name: " + Name;
}
Такой синтаксис означает, что у типа Student нет публичного свойства
PrintInfo, но при преобразовании к типу интерфейса будет использоваться
заданная функция. Этот способ скрыть реализацию интерфейса используют, например, в ситуации, когда класс должен реализовать два интерфейса с одинаковыми именами функций и разными сигнатурами.
10.2. Интерфейс IDisposable
Интерфейс IDisposable является одним из базовых в C#. Он предназначен для обеспечения возможности освобождения ресурсов классом, реализующим интерфейс. Интерфейс требует реализации единственной
функции void Dispose(), которая будет вызываться для освобождения ресурсов (в каком-то смысле эта функция выполняет роль деструктора C++).
Эта функция может, конечно, вызываться явно, но для обеспечения правильного времени жизни экземпляра в C# имеется специальная синтаксическая поддержка – оператор using.
using (var file = File.OpenText("input.txt"))
{
string str;
while ((str = file.ReadLine()) != null)
{
Console.WriteLine(file.ReadLine());
}
}
60
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
