
Объектно-ориентированное программирование.-6
.pdf
{
value.X();
}
};
Этот код компилятор C++ воспринимает как корректный. Ошибка может возникнуть позже, когда мы укажем в качестве параметра тип, не имеющий метода X().
В языке C# код класса должен быть изначально написан таким образом, чтобы он работал с любым типом. Если не указаны ограничения параметров типа (см. п. 5.1.2), то очевидно, что к параметрам типа можно применять только те операции, которые применимы ко всем типам .NET. Например:
class MyClass5<T>
{
T value = 0; // Ошибка public MyClass5(T b)
{
T a;
a = b; // ОК
if (a == b) // Ошибка
{
a = null; // Ошибка if (a == null) // ОК
{
b.GetType(); // ОК
}
}
}
}
Т.к. не все типы допускают неявное преобразование из типа int, компилятор C# сразу выдает ошибку для «value = 0», хотя, вполне возможно, мы бы использовали этот класс только для числовых параметров типа. Операция «a = b» компилируется без ошибки, т.к. присвоение поддерживают все типы данных. Операция сравнения «a == b» не компилируется, т.к. не все типы ее поддерживают (например, структуры). Операция присваивания «a = null» также вызывает ошибку, т.к. пустой ссылкой можно инициализировать только обнуляемые и ссылочные типы, а типы по значению – нет. Операция сравнения «a == null» разрешена. Сравнить с пустой ссылкой можно экземпляр любого типа, просто для типа по значению результатом всегда будет false. Ну и метод GetType() определен для любого типа, т.к. наследуется от корневого класса Object.
351

5.1.1.3.Инициализация значениями по умолчанию
Вуниверсальных типах и методах одной из существующих проблем является назначение параметризованному типу T значения по умолчанию, если заранее неизвестны следующие моменты:
• Является ли T ссылочным типом или типом значения;
• Если T является типом значения, будет ли он числовым значением или структурой.
Очевидно, что для всех этих трех случаев инициализация существенно отличается. Для ссылочного типа значением по умолчанию является null, для числовых типов – аналог нуля для соответствующего типа данных (см. § 3.1), для структур необходимо все поля инициализировать значениями по умолчанию. Например, напишем класс-вектор с произвольным типом аргументов:
class Vector<T>
{
private T[] elems;
public Vector(int size)
{
elems = new T[size];
}
public void Reset()
{
for (int i = 0; i < elems.Length; i++)
{
// elems[i] = ???;
}
}
public T this[int i]
{
get { return elems[i]; } set { elems[i] = value; }
}
public int Length
{
get { return elems.Length; }
}
}
По замыслу, метод Reset должен сбрасывать значения элементов вектора. Но что написать справа от оператора присваивания? На значение любого типа компилятор будет выдавать ошибку. Нет такого типа данных, который имеет явное или неявное преобразование ко всем другим типам.
Для этого используется ключевое слово default. О нем мы уже говори-
352

ли в § 3.1. Выражение default(<тип>) возвращает значение по умолчанию для указанного типа данных:
elems[i] = default(T);
5.1.1.4. Наследование от универсальных классов и интерфейсов
Введем понятие открытого типа для универсальных типов, имеющих формальные параметры (например, Vector<T>). После указания фактических параметров получаем закрытый тип (например, Vector<int>).
При наследовании от универсального типа мы можем получить либо универсальный тип, используя наследование открытого типа, либо конкретный тип при наследовании закрытого типа. Впрочем, производный тип может объявлять свои параметры типа, не зависящие от базового типа. Пример:
class BaseClass { } |
|
|
|
class BaseGenClass<T> { |
} |
|
|
class ChildClass1 : BaseClass { } // ОК |
|
|
|
class ChildClass2 : BaseGenClass<int> { |
} // ОК |
||
class ChildClass3 : BaseGenClass<T> { } |
// Ошибка |
||
class ChildGenClass1<T> |
: BaseClass { } |
// ОК |
|
class ChildGenClass2<T> |
: BaseGenClass<T> { } |
// ОК |
|
class ChildGenClass3<U> |
: BaseGenClass<T> { } |
// Ошибка |
|
class ChildGenClass4<U> |
: BaseGenClass<int> { |
} // ОК |
|
class ChildGenClass5<T, |
U> : BaseGenClass<T> { } // ОК |
class ChildSubClass<T> : ChildGenClass5<T, int> { } // ОК class ChildSubClass : ChildGenClass5<double, int> { } // ОК
Открытые и закрытые типы можно использовать в качестве параметров методов:
static void Swap<T>(Vector<T> a, Vector<T> b, int idx)
{
T x = a[idx]; a[idx] = b[idx]; b[idx] = x;
}
static void Swap(Vector<int> a, Vector<double> b, int idx)
{
int x = a[idx]; a[idx] = (int)b[idx]; b[idx] = x;
}
Если универсальный класс или структура реализует интерфейс, все экземпляры этого класса или структуры могут быть приведены к этому интерфейсу:
353

class CloneClass<T> : ICloneable
{
public object Clone()
{
return new CloneClass<T>();
}
}
static object CreateCopy(ICloneable i)
{
Console.WriteLine("Копируем " + i); return i.Clone();
}
static int Main()
{
CloneClass<int> ci = new CloneClass<int>(); CloneClass<string> cs = new CloneClass<string>();
//Копируем GenericTypesSample.Program+CloneClass`1[System.Int32] CreateCopy(ci);
//Копируем GenericTypesSample.Program+CloneClass`1[System.String] CreateCopy(cs);
return 0;
}
Универсальные типы инвариантны. Другими словами, если имеем класс Vector<производный класс>, при попытке представить его как Vector<базовый класс> возникнет ошибка компиляции:
static void OutVectorS(Vector<string> v)
{
for (int i = 0; i < v.Length; i++)
{
Console.WriteLine(v[i]);
}
}
static void OutVectorO(Vector<object> v)
{
for (int i = 0; i < v.Length; i++)
{
Console.WriteLine(v[i]);
}
}
static int Main()
{
Vector<string> vs = new Vector<string>(3);
OutVectorS(vs); OutVectorO(vs); // Ошибка return 0;
}
354

5.1.2. Ограничения параметров типа
При определении универсального члена можно ограничить виды типов, которые могут использоваться клиентским кодом в качестве аргументов типа при инициализации соответствующего типа или вызове метода. При попытке клиентского кода нарушить ограничения возникает ошибка компиляции. Это называется ограничениями параметров типа. Ограничения определяются с помощью контекстно-зависимого ключевого слова where:
<ограничения параметров типа> :: <список ограничений1> [<список ограничений2>] [...]
<список ограничений> :: where <идентификатор> : <первичное ограничение> [, <вторичные ограничения>] [, <ограничение конструктора>]
<список ограничений> :: where <идентификатор> : <вторичные ограничения> [, <ограничение конструктора>]
<список ограничений> :: where <идентификатор> : <ограничение конструктора>
<первичное ограничение> :: struct <первичное ограничение> :: class <первичное ограничение> :: <тип класса>
<вторичные ограничения> :: <вторичное ограничение> [, ...] <вторичное ограничение> :: <тип интерфейса> <вторичное ограничение> :: <идентификатор>
<ограничение конструктора> :: new()
Таким образом, можно выделить шесть видов ограничений (табл. 5.1).
|
Табл. 5.1 – Виды ограничений параметров типа |
|
|
Ограничение |
Описание |
|
|
where T : |
Фактический тип должен быть типом по значению (кро- |
struct |
ме обнуляемых типов) |
|
|
|
|
where T : class |
Фактический тип должен быть ссылочным (классом, ин- |
|
терфейсом, делегатом, массивом и т.п.) |
|
|
where T : new() |
Фактический тип должен иметь открытый (public) кон- |
|
структор по умолчанию (без параметров) |
|
|
where T : <тип |
Фактический тип должен являться указанным классом |
класса> |
или быть производным от него. Класс не должен быть |
|
|
|
Object, Array, Delegate, Enum или ValueType, изолиро- |
|
ванным или статическим |
|
|
where T : <тип |
Фактический тип должен являться указанным интерфей- |
интерфейса> |
сом или реализовывать его |
|
|
|
|
|
355 |

where T : U
Фактический параметра типа T должен являться типом фактического параметра U или быть производным от него (неприкрытое ограничение типа)
К одному параметру типа может применяться несколько ограничений, при этом сами ограничения могут быть универсального типа. При наложении ограничений на параметр типа увеличивается число допустимых операций и вызовов методов, поддерживаемых ограничивающим типом и всеми типами в его иерархии наследования. Пример:
class MyClass1<T> where T : class
{
T a;
public MyClass1(T b)
{
a = new T(); // Ошибка a = b;
}
}
class MyClass2<T> where T : new()
{
public T a = new T(); // ОК
}
class MyClass3<T> where T : Int32, Int64 { } // Ошибка class MyClass4<T> where T : String { } // Ошибка
interface IRandomize<T>
{
T Random();
}
class Rnd : IRandomize<int>
{
static double Last = 0;
public int Random()
{
double r;
if (Last == 0) Last = DateTime.Now.Ticks % 1000000; Last = Math.Sqrt(Last);
r = Last - (int)Last; Last = r*1000000.0;
return (int)(r*int.MaxValue);
}
}
class StdRandom : IRandomize<double>
{
public static Random r = new Random();
public double Random()
{
return r.NextDouble();
356

}
}
class MyClass5<T, U>
where T : IRandomize<U>, new() where U : IComparable
{
private T randomizer;
public MyClass5()
{
randomizer = new T();
}
public bool CompareToRandom(U min, U max, out U rnd)
{
rnd = randomizer.Random();
return rnd.CompareTo(min) >= 0 && rnd.CompareTo(max) <= 0;
}
}
class Program
{
static int Main()
{
MyClass1<string> cls1 = new MyClass1<string>("!!!"); // ОК MyClass1<int?> cls2 = new MyClass1<int?>(null); // Ошибка
MyClass2<int> cls3 = new MyClass2<int>(); // ОК MyClass2<MyClass1<object>> cls4 = new
MyClass2<MyClass1<object>>(); // Ошибка
MyClass5<Rnd, int> cls5 = new MyClass5<Rnd, int>(); // ОК MyClass5<StdRandom, int> cls6 = new MyClass5<StdRandom,
int>(); // Ошибка
MyClass5<StdRandom, double> cls7 = new MyClass5<StdRandom,
double>(); // ОК
int irnd; double frnd; bool bi, bf;
for (int i = 0; i < 10; i++)
{
bi = cls5.CompareToRandom(500000000, 1500000000, out
irnd);
bf = cls7.CompareToRandom(0.25, 0.75, out frnd);
Console.WriteLine(bi ? "{0} є [500000000, 1500000000]" : "{0} є [0, 499999999]U[1500000001, 2147483647]", irnd);
Console.WriteLine(bf ? "{0} є [0.25, 0.75]" : "{0} є
[0, 0.25)U(0.75, 1]", frnd);
}
return 0;
}
}
Ошибки:
1) В типе T не гарантировано наличие конструктора по умолчанию;
357

2)Int32 и Int64 – структуры, а не классы;
3)Использование класса String в ограничениях запрещено;
4)Обнуляемый тип не является ссылочным;
5)В классе MyClass1<object> нет конструктора по умолчанию;
6)Класс StdRandom не наследует интерфейс IRandomize<int>.
В случае применения ограничения «where T : class» следует избегать использования операторов «==» и «!=» с параметром типа, потому что эти операторы выполняют только сравнение ссылок. Это верно даже в том случае, если эти операторы перегружаются в типе, который используется в качестве аргумента. Причиной такого поведения является то, что во время компиляции компилятор знает только, что T является ссылочным типом, и поэтому должен использовать операторы по умолчанию, которые действительны для всех ссылочных типов. Если необходимо проверять равенство содержимого объектов, рекомендуется применить ограничение «where T : IComparable<T>».
Если интерфейс указан в качестве ограничения параметра типа, могут использоваться лишь типы, реализующие интерфейс.
Универсальные типы, унаследованные от открытых базовых типов, должны указывать ограничения, которые соответствуют ограничениям базового типа или являются более строгими:
class MyClass6<T> where T : IEnumerable, ICollection, new() { } class MyClass7<T> : MyClass6<T> where T : new() { } // Ошибка class MyClass8<T> : MyClass6<T> where T : IEnumerable<T>,
ICollection<T>, new() { } // Ошибка
class MyClass9<T> : MyClass6<T> where T : IList, new() { } // ОК class MyClass10<T> : MyClass6<T> where T : IList { } // Ошибка
Комментарии:
1)Класс MyClass7 не гарантирует, что тип T реализует интерфейсы
IEnumerable и ICollection, а этого требует класс MyClass6.
2)Класс MyClass8 не гарантирует, что тип T реализует интерфейс
ICollection. Интерфейс IEnumerable<T> наследуется от IEnumerable, поэтому это ограничение выполнено. Но интерфейс ICollection<T> наследует только
IEnumerable<T> и IEnumerable, а ICollection – нет.
3)Ограничения класса MyClass9 описаны правильно, т.к. интерфейс
IList наследует интерфейсы IEnumerable и ICollection.
4)Класс MyClass10 не гарантирует, что тип T будет иметь конструктор по умолчанию, а этого требует класс MyClass6.
358
Пример: Samples\5.1\5_1_2_where.
5.1.3. Стандартные универсальные типы
Универсальные типы предоставляют решение проблем более ранних версий среды CLR и языка C#, в которых обобщения достигались за счет приведения типов к универсальному базовому типу Object и из него. Путем создания универсального класса можно создать коллекцию, которая была бы строго типизирована во время компиляции.
Ограничения, возникающие при использовании конкретных классов коллекции, можно продемонстрировать класса коллекции ArrayList. Это очень удобный класс коллекции, который можно использовать без изменений для хранения элементов как типов по значению, так и ссылочных типов. Однако, за удобство приходится платить. Любые типы, добавляемые к классу коллекции ArrayList, неявно приводятся к Object. Если в качестве элементов выступают типы по значению, то для них необходимо выполнять операции упаковки и распаковки (см. п. 3.1.5). Операции приведения, упаковки и распаковки снижают производительность. Еще одно ограничение состоит в отсутствии проверки типа во время компиляции. Поскольку класс коллекции ArrayList приводит все элементы к типу Object, невозможно предотвратить выполнение клиентским кодом во время компиляции действий, связанных с неверной интерпретацией типов элементов.
Поэтому необходимо для класса коллекции ArrayList и других подобных классов реализовать возможность указания для каждого отдельного экземпляра в клиентском коде определенного типа данных, который они должны использовать. Это устранит необходимость приведения к типу System.Object и даст компилятору возможность проверки типа. Другими словами, классу коллекции ArrayList необходим параметр типа. Таким вариантом этого класса является коллекция List<T>. С помощью данного класса можно создать список, который будет не только безопаснее, чем ArrayList, но и существенно быстрее, особенно если элементами списка являются экземпляры типов по значению.
Библиотека классов платформы .NET Framework содержит несколько новых универсальных классов коллекций в пространстве имен System.Collections.Generic. Их следует использовать по мере возможности вместо таких классов, как ArrayList в пространстве имен System.Collections.
359
§ 5.2. Потоки
Для разделения различных выполняемых приложений в операционных системах используются процессы. Потоки являются основными элементами, для которых операционная система выделяет время процессора; внутри процесса может выполняться более одного потока. Каждый поток поддерживает обработчик исключений, планируемый приоритет и набор структур, используемых системой для сохранения контекста потока во время его планирования. Контекст потока содержит все необходимые данные для возобновления выполнения (включая набор регистров процессора и стек) в адресном пространстве ведущего процесса.
5.2.1. Основы организации потоков
По умолчанию, все разрабатываемые нами приложения являются однопоточными. Многопоточность позволяет приложениям разделять задачи и работать над каждой независимо, чтобы максимально эффективно задействовать процессор и пользовательское время. Однако, чрезмерное злоупотребление многопоточностью может снизить эффективность программы. Разделять процесс на потоки следует только в том случае, если это оправданно.
5.2.1.1. Потоки и многозадачность
Поток является единицей обработки данных, а многозадачность – это одновременное исполнение нескольких потоков. Существует два вида многозадачности – совместная (cooperative) и вытесняющая (preemptive). Самые ранние версии Microsoft Windows поддерживали совместную многозадачность. Это означало, что каждый поток отвечал за возврат управления процессору, чтобы тот смог обработать другие потоки. Т.е., если какой-либо поток не возвращал управление (из-за ошибки в программе или по другой причине), другие потоки не могли продолжить выполнение. И если этот поток «зависал», то «зависала» вся система.
Однако, начиная с Windows NT, стала поддерживаться вытесняющая многозадачность. При этом процессор отвечает за выдачу каждому потоку определенного количества времени, в течение которого поток может выполняться – кванта времени (timeslice). Далее процессор переключается между
360