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

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

.pdf
Скачиваний:
1
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
В приведенном примере переменная file имеет тип StreamReader, ко­торый реализует интерфейс IDisposable. По окончании блока using вызы­вается Dispose, где закрывается и освобождается файл.
Заметим, что у оператора using имеется другая форма, без оператор­ных скобок:
using var file = File.OpenText("input.txt");
В этом случае Dispose вызывается в конце блока, в котором находится опе- ратор using. Dispose вызывается в конце using даже в том случае, когда внутри блока происходит исключение.
Обратим внимание на то, что рекомендованная реализация интер­фейса IDisposable выглядит следующим образом:
public class DisposeExample : IDisposable
{ private bool disposedValue; protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // вызов Dispose у всех дочерних полей и свойств класса }
disposedValue = true; } } ~DisposeExample() => Dispose(false); public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } }
// освобождение всех остальных ресурсов
Дополнительно мы не рекомендуем использовать финализаторы и об­ращения к сборщику мусора нигде, кроме такой реализации IDisposable, пока не будет абсолютно понятно, как это должно работать.
61
11. УНИВЕРСАЛЬНЫЕ ШАБЛОНЫ
значением. Не может
() и
unmanaged
держка
nullable
)
11.1. Синтаксис шаблонов
При написании программ достаточно часто возникает желание ис­пользовать один и тот же алгоритм для разных типов данных. В этом слу­чае тип данных – это один из параметров алгоритма. Для синтаксической поддержки такой возможности в C# классы, структуры, интерфейсы, деле­гаты и методы могут быть универсальными шаблонами, имея один или не­сколько типов в качестве параметра. Например, универсальный класс мо­жет иметь вид
public class Generic<T1,T2>{public T1 Field;public T2 Function(T1 parameter);}
Здесь T1 и T2 – типы-параметры, которые необходимо задать при создании экземпляра класса.
При описании универсального класса могут быть заданы ограничения на типы-параметры. Например, описание
class PrintData<T> where T:IPerson
{ void Print(T person) => Console.WriteLine(person.PrintInfo); }
требует, чтобы параметр типа реализовывал интерфейс IPerson. Обратим внимание, что, в отличие от C++, в языке C# нельзя обратиться к функции типа-параметра, если на тип не наложено ограничение по реализации со­ответствующего интерфейса, даже если такая функция существует у класса, который указан при создании экземпляра.
Приведем список возможных ограничений параметров универсаль­ного класса:
where T : struct
Параметр должен быть типом­комбинироваться с ограничением new
where T : class Параметр должен быть ссылочным типом (не до-
пускающим значение null, если включена под-
62
where T : class?
Параметр должен быть произвольным ссылочным
ния
null)
нием)
и
new()
ров
имя класса, то параметр должен
быть этим классом или его наследником. Если
параметр должен соответствовать
.
Ограничений на интерфейсы может быть
несколько
типом (допускающим или не допускающим значе-
where T : notnull Параметр не может допускать значение null (и мо-
жет быть как ссылочным типом, так и типом-значе-
where T : unmanaged Параметр не может допускать значение null и мо-
жет быть арифметическим, логическим или enum- типом, а также типом struct (у которого все поля un- managed). Не может комбинироваться с struct
where T : new() Параметр должен иметь конструктор без парамет-
where T : <name>
Если <name> –
where T : <name>?
<name> – интерфейс, то параметр обязан реализо­вывать этот интерфейс. При включенной под­держке nullable указанному в ограничении допустимости null
11.2. Ковариантность и контравариантность
Термины «ковариантность» и «контравариантность» относятся к воз­можности приведения типа параметрического шаблона к шаблону с дру­гим типом в качестве параметра. Ковариантными и контравариантными могут быть интерфейсы и делегаты.
Ковариантные типы описываются с ключевым словом out:
interface IEnumerable<out T>{<тело интерфейса>}
Такое описание означает, что если класс Derived является наследником класса Base, то переменной типа IEnumerable<Base> можно присвоить эк­земпляр типа IEnumerable<Derived>. Естественно, описание типа как ко­вариантного накладывает некоторые ограничения на его использование в
63
теле интерфейса. Например, он может использоваться в качестве типа воз­вращаемого значения метода, но не может использоваться в качестве типа его параметра.
Контравариантные типы описываются с ключевым словом in:
delegate void Action<in T>(T obj);
Контравариантность в данном случае означает, что переменной типа Action<Derived> можно присвоить экземпляр типа Action<Base>. Конечно, контравариантность типа-параметра, как и ковариантность, накладывает определенные ограничения на его использование в теле ин­терфейса. Например, он не может быть типом возвращаемого значения ме­тода, но может быть типом его параметров.
Заметим, что универсальный тип может иметь несколько параметров, и у любого из них может быть указан in или out. Например, существует стандартный тип делегата Func<in T1,in T2, out TOut> (в принципе, он су­ществует для различного числа параметров T1 … Tn, вплоть до 16), кото­рый может получать значения метода с двумя параметрами типов T1 и T2 и возвращаемое значение типа TOut. В силу ковариантности и контравари­антности параметров переменная типа Func<in T1,in T2, out TOut> может принимать значения метода, у которого параметры являются базовыми классами для типов T1 и T2, а возвращаемое выражение – наследником класса TOut. Например:
record Person(string Name); record Teacher (string Name, string Position, string Faculty) : Person(Name);
class Test { static void Main() { Teacher person = new Teacher("Федор", "Профессор", "ФПМИ"); Console.WriteLine(person); Func<Teacher, string, Person> CreateNew = (Teacher t, string s) => t with { Name=s }; Console.WriteLine(CreateNew(person,"Петр")); // CreateNew = (Person t, string s) => new Teacher(t.Name, s, "ФПМИ") // так нельзя! Делегат не того типа! // а вот так можно: CreateNew = (Func<Person, string, Teacher>)((Person t, string s) => new Teacher(t.Name, s, "ФПМИ")); Console.WriteLine(CreateNew(person, "Доцент")); } }
64
Результат работы этой программы:
Teacher { Name = Федор, Position = Профессор, Faculty = ФПМИ } Teacher { Name = Петр, Position = Профессор, Faculty = ФПМИ } Teacher { Name = Федор, Position = Доцент, Faculty = ФПМИ }
11.3. Интерфейсы перечисления. Оператор foreach
Особую роль в языке C# играют интерфейсы IEnumerator и IEnumera­ble, а также их типизированные наследники IEnumerator<out T> и IEnu­merable<out T>. Эти шаблоны являются основой практически всех коллек-
ций (массивов, очередей, множеств и т. д.) языка.
Интерфейс IEnumerator соответствует итератору в других языках и предназначен для перемещения по коллекциям объектов. Он содержит одно свойство object Current {get;}, которое должно возвращать текущий объект коллекции, и две функции:
1) bool MoveNext(), которая должна изменить текущий объект на сле-
дующий и вернуть true при успешном выполнении и false, если коллекция закончилась;
2) void Reset(), которая должна установить итератор в начальное со-
стояние.
Обратим внимание, что в начальном состоянии текущий объект не определен (в стандартных коллекциях возвращается null), и первый вызов MoveNext() устанавливает Current в начальный элемент коллекции (для не- пустой коллекции, для пустой MoveNext() вернет false).
Интерфейс IEnumerator<out T> наследуется от IEnumerator и отлича­ется тем, что свойство Current имеет тип не object, а T. Кроме того, IEnu- merator<out T> наследуется еще и от IDisposable.
Коллекции объектов C# обычно реализуют интерфейс IEnumerable. Этот интерфейс содержит единственную функцию IEnumerator GetEnu-
merator(). Типизированный интерфейс IEnumerable<out T> наследуется от IEnumerable и отличается тем, что GetEnumerator() возвращает типизиро-
ванный итератор IEnumerator<T>.
Для цикла по элементам коллекции в языке существует специальный оператор foreach. Этот оператор имеет вид
foreach (<описание типа переменной> <имя переменной> in <выражение>) <тело цикла>
65
Выражение после ключевого слова in должно возвращать экземпляр типа, у которого есть функция GetEnumerator, возвращающая экземпляр типа, у которого есть свойство Current (возвращающее экземпляр типа, приводимого к типу, указанному в <описание типа переменной>) и функ- ция MoveNext без параметров, возвращающая bool . Интересно, что после 8-й версии языка экземпляр типа коллекции может не иметь интерфейс IEnumerable, достаточно наличия функции GetEnumerator, причем возвра­щающей необязательно IEnumerator, а просто любой тип с наличием Current и MoveNext с правильной сигнатурой.
Вообще, такая неявная реализация интерфейсов (без явного указания интерфейса) иногда называется «утиная типизация» (от английской пого­ворки If it looks like a duck, swims like a duck and quacks like a duck, then it probably is a duck).
В C# «утиная типизация» для оператора foreach введена для того, чтобы цикл можно было использовать с типами, принципиально не имею­щими возможности явно реализовывать интерфейсы, например ref struct, который мы обсудим в следующей книге.
Выполнение оператора foreach (V v in x) <тело цикла> в соответствии с документацией практически эквивалентно следующей последовательно­сти операторов (естественно, в теле цикла переменная e использоваться не может):
{ var e = x.GetEnumerator();
try
{ while (e.MoveNext()) { V v = e.Current; <тело цикла> } }
finally
{ if(e is IDisposable edisp)edisp.Dispose(); }
}
Приведем небольшой (и немного искусственный) пример с реализа­цией описанных интерфейсов коллекции:
using System.Collections; using System.Collections.Generic;
66
var people = new Persons(); foreach (var person in people) Console.WriteLine(person); record Person(string Name); class Persons : IEnumerable<Person>
{ Person Person1 = new Person("Федор"); Person Person2 = new Person("Петр"); Person Person3 = new Person("Егор"); class PersonEnumerator : IEnumerator<Person> { int selected = -1; Persons people; public PersonEnumerator(Persons people) { this.people = people; } public Person Current => selected switch { 0 => people.Person1, 1 => people.Person2, 2 => people.Person3, _ => throw new Exception("Недопустимое значение") }; public bool MoveNext() => ++selected < 3; public void Reset() => selected = -1; object IEnumerator.Current => Current; public void Dispose() { } } public IEnumerator<Person> GetEnumerator() => new PersonEnumerator(this); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); }
Результат работы этой программы:
Person { Name = Федор } Person { Name = Петр } Person { Name = Егор }
Все массивы реализуют интерфейс перечисления IEnumerable, по­этому могут использоваться в цикле foreach. При этом многомерные мас­сивы перечисляют свои элементы так, что быстрее всего изменяется по­следний индекс, затем предпоследний и т. д. То есть, например, двумер­ный массив (матрица) перечисляется строками.
67
11.4. Формирование перечисления функцией. Оператор yield return
В языке C# достаточно много возможностей связано с использова­нием коллекций элементов. При этом реализация собственных типов кол­лекций «вручную», как в последнем примере, задача достаточно редкая, поскольку стандартные коллекции существуют почти на все возможные случаи. Гораздо чаще возникает необходимость перечислять элементы ка­кой-либо коллекции, вычисляя их без явного хранения. Например, нужна коллекция, содержащая элементы последовательности Фибоначчи. Есте­ственно, можно создать массив нужной размерности и заполнить его зара­нее. А можно поступить иначе:
foreach (var num in Numbers()) Console.WriteLine(num);
IEnumerable<int> Numbers()
{ int a = 1, b = 1; yield return a; yield return b; while (true) { try { (a, b) = (b, checked(a + b)); } catch (OverflowException) { yield break; } yield return b; } }
Оператор yield return возвращает следующий элемент последователь­ности, и выполнение программы приостанавливается до обращения за сле­дующим элементом последовательности. Если при этом необходимо пре­рвать генерацию элементов последовательности, то можно использовать оператор yield break. Приведенная программа печатает последователь­ность чисел Фибоначчи до тех пор, пока числа помещаются в тип int. Для этого мы использовали выражение checked (см. раздел 2.1, с. 14).
68
СПИСОК РЕКОМЕНДУЕМОЙ ЛИТЕРАТУРЫ
1. Рихтер Дж. CLR via C#. Программирование на платформе Microsoft
.NET Framework 4.5 на языке C#. – 4-е изд. / Дж. Рихтер. – Санкт-Пе­тербург: Питер, 2013. – 896 с. – ISBN 978-5-496-00433-6.
2. Албахари Дж. C# 9.0. Справочник. Полное описание языка. / Дж. Ал-
бахари – Москва: Диалектика, 2021. – 1056 с. – ISBN978-5-907365-81-0.
3. Троелсен Э. Язык программирования C# 9 и платформа .NET 5. /
Э. Троелсен, Ф. Джепикс. – Москва: Диалектика, 2022. – 1392 с. – ISBN 978-5-907458-67-3.
Рояк Михаил Эммануилович
Ступаков Илья Михайлович
ОСНОВЫ ОБЪЕКТНО ОРИЕНТИРОВАННОГО
ПРОГРАММИРОВАНИЯ НА ЯЗЫКЕ C#
БАЗОВЫЕ СВЕДЕНИЯ О ЯЗЫКЕ
Учебное пособие
Выпускающий редактор И.П. Брованова
Корректор Л.Н. Киншт Дизайн обложки А.В. Ладыжская Компьютерная верстка М.Э. Рояк
Налоговая льгота – Общероссийский классификатор продукции
Издание соответствует коду 95 3000 ОК 005-93 (ОКП)
Подписано в печать _08.12.2023. Формат 60 × 84 1/16. Бумага офсетная. Тираж 150 экз.
Уч.-изд. л. 4,18 Печ. л. 4,5 Изд. № 240. Заказ № 317. Цена договорная
Отпечатано в типографии
Новосибирского государственного технического университета
630073, г. Новосибирск, пр. К. Маркса, 20
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]