Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие

.pdf
Скачиваний:
1
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Если начать увеличивать размерность массива 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-mem­bers).
Заметим, что при реализации интерфейса класс может скрыть некото­рые функции интерфейса, реализовав их явно, с указанием интерфейса, например:
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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]