Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы объектно ориентированного программирования на языке С# базовые сведения о языке. Учебное пособие
.pdf
В приведенном примере переменная 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 и IEnumerable, а также их типизированные наследники IEnumerator<out T> и IEnumerable<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
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
