
Объектно-ориентированное программирование.-6
.pdf
public VowelEnum(string s) { S = s; }
public void Reset()
{
pos = -1;
}
public bool MoveNext()
{
pos = S.IndexOfAny("аеёиоуыэюя".ToCharArray(), pos + 1); return pos >= 0 && pos < S.Length;
}
public object Current
{
get { return S[pos]; }
}
}
class Word : IEnumerable
{
public string S = string.Empty; public Word(string s) { S = s; }
public IEnumerator GetEnumerator()
{
return new VowelEnum(S);
}
}
static int Main()
{
string str = "Будем искать в этой фразе гласные буквы!";
Word w = new Word(str);
Console.Write("Гласные: ");
foreach (char c in w) Console.Write("{0} ", c); Console.WriteLine();
return 0;
}
В этом примере в качестве итератора выступает класс Word, инкапсулирующий строку, а в качестве перечислителя – класс VowelEnum, перебирающий в строке все гласные буквы. Вывод на консоль:
Гласные: у е и а э о а е а ы е у ы
Управляемые итераторы
Как уже было сказано в п. 3.4.4.6, ключевое слово yield может быть использовано для создания управляемого итератора. При этом конструкция yield return возвращает следующий элемент итератора, а yield break сообщает о конце итераций.
Пример:
341

public static IEnumerable VowelIterator(string s)
{
int pos = -1; do
{
pos = s.IndexOfAny("аеёиоуыэюя".ToCharArray(), pos + 1); if (pos >= 0 && pos < s.Length) yield return s[pos]; else yield break;
} while (true);
}
static int Main()
{
string str = "Будем искать в этой фразе гласные буквы!";
Console.Write("Ещё раз: ");
foreach (char c in VowelIterator(str)) Console.Write("{0} ", c); Console.WriteLine();
return 0;
}
Эффект от этого кода такой же, как и в предыдущем примере:
Ещё раз: у е и а э о а е а ы е у ы
Коллекции
Интерфейс ICollection определяет размер, перечислители и методы синхронизации для всех нестандартных коллекций. Он наследует интерфейс IEnumerable. Интерфейс IList представляет коллекцию объектов с индивидуальным доступом, осуществляемым при помощи индекса. Он наследует интерфейсы ICollection и IEnumerable. Все эти интерфейсы объявлены в про-
странстве имен System.Collections.
Также есть интерфейс универсальной коллекции IList<T>, объявленный в пространстве имен System.Collections.Generic. Об универсальных типах мы будем говорить в следующей главе.
4.9.4.3. Копирование объектов
Для создания новой копии объекта, а не копирования ссылки в операции присвоения, используется реализация интерфейса ICloneable. В нем определен единственный метод:
object Clone();
Для примера обеспечим копирование дробей:
class Rational : ICloneable
{
/* описанное ранее тело класса */
342

public object Clone()
{
return new Rational(Numerator, Denominator);
}
public Rational Copy()
{
return (Rational)Clone();
}
}
static int Main()
{
/* описанное ранее использование класса */
r[1] = r[0];
r[2] = (Rational)r[0].Clone(); r[0].Numerator = 7; Console.WriteLine(r[1]); // 7/6 Console.WriteLine(r[2]); // -1/6 return 0;
}
Экземпляр r[1] является ссылкой на r[0], а r[2] – копией. Чтобы не требовалось преобразование типа при копировании, можно использовать метод
Copy.
4.9.4.4. Форматирование объектов
Интерфейс IConvertible определяет методы, которые преобразуют значение реализующего этот метод ссылочного типа или типа значения в другой тип данных. В этом интерфейсе описаны следующие методы:
TypeCode GetTypeCode();
<тип> To<класс>(IFormatProvider provider);
object ToType(Type conversionType, IFormatProvider provider);
Первый возвращает код типа объекта (см. п. 3.1.2), а остальные методы обеспечивают преобразование к ряду типов.
Пример:
class Rational : IConvertible
{
/* описанное ранее тело класса */
public TypeCode GetTypeCode()
{
return TypeCode.Object;
}
public bool ToBoolean(IFormatProvider provider)
{
return Numerator != 0 && Denominator != 0;
343

}
public byte ToByte(IFormatProvider provider)
{
return (byte)(Numerator / Denominator);
}
public char ToChar(IFormatProvider provider)
{
throw new InvalidCastException("Ошибка преобразования Rational в char");
}
public double ToDouble(IFormatProvider provider)
{
return (double)Numerator / Denominator;
}
public string ToString(IFormatProvider provider)
{
return ToString();
}
}
static int Main()
{
/* описанное ранее использование класса */
Console.WriteLine(r[0].ToSingle(null)); // 1,166667 return 0;
}
Здесь перечислены не все методы ввиду их большого количества. При преобразовании к булевому типу значение дроби будет считаться истиной, если ее числитель и знаменатель не равны нулю. Для целых типов данных делим числитель на знаменатель, преобразуем тип результата и возвращаем его. Для типов с плавающей точкой сначала преобразуем числитель к требуемому типу, и только потом делим, чтобы деление не было целочисленным. Преобразование к строке мы уже определили ранее в методе ToString(). Если формат не поддерживается, генерируем исключительную ситуацию
InvalidCastException. Интерфейс IFormatProvider в данном случае не исполь-
зуется, однако, если преобразование зависит от каких-то дополнительных опций (формат значения, региональные параметры и т.п.), можно указать экземпляр класса, его реализующий.
Интерфейс IFormattable используется для преобразования произвольного типа в строку, и определяет только один метод:
string ToString(string format, IFormatProvider formatProvider);
Пример:
344

class Rational : IFormattable
{
/* описанное ранее тело класса */
public string ToString(string format, IFormatProvider formatProvider)
{
if (format == null || format[0] == 'G') return Numerator.ToString(format, formatProvider) + "/" + Denominator.ToString(format, formatProvider);
else if (format[0] == 'F') return ToDouble(null).ToString(format, formatProvider);
else if (format[0] == 'D') return ToInt32(null).ToString(format, formatProvider);
else throw new FormatException("Недопустимый формат");
}
static int Main()
{
/* описанное ранее использование класса */
Console.WriteLine(String.Format("{0:F}", r[0])); // 1,17 Console.WriteLine(String.Format("{0:F4}", r[0])); // 1,1667 Console.WriteLine(r[0].ToString("F", new CultureInfo("en-US")));
// 1.17
Console.WriteLine(String.Format("{0:D}", r[0])); // 1 return 0;
}
Интерфейс IFormatProvider предоставляет механизм извлечения объекта для управления форматированием. Как уже отмечалось (см. п. 3.2.1.1), его реализуют три класса .NET – NumberFormatInfo, DateTimeFormatInfo и CultureInfo. В принципе, нет необходимости создавать новые реализации данного интерфейса. Однако, если это потребуется, нужно будет реализовать лишь один его метод:
object GetFormat(Type formatType);
Дополнительную информацию можно найти в библиотеке MSDN.
4.9.4.5. Управление ресурсами
В языке C# предусмотрено использование оператора using для описания блока управления ресурсами (см. п. 3.5.3). При этом управляемые ресурсы должны наследовать интерфейс IDisposable. Учитывая, что в среде CLR удалением неиспользуемых ресурсов занимается сборщик мусора, задача ручного управления освобождения ресурсов возникает не так уж часто.
Обычно это требуется в том случае, если при выполнении какой-либо операции объект использует неуправляемые ресурсы, особенно в критических объемах (большой объем памяти, множество потоков ввода-вывода и
345

т.п.). В этом случае, после завершения операции ресурсы можно освободить, но когда сборщик мусора сделает это – неизвестно.
Обычно код выглядит так:
public class MyResource: IDisposable
{
private bool disposed;
public MyResource()
{
// Выделяем ресурсы disposed = false;
}
public void Dispose()
{
Dispose(true); GC.SuppressFinalize(this);
}
private void Dispose(bool managed)
{
if (!disposed)
{
if (managed)
{
// Освобождаем управляемые ресурсы
}
// Освобождаем неуправляемые ресурсы disposed = true;
}
}
~MyResource()
{
Dispose(false);
}
}
Метод GC.SuppressFinalize запрещает сборщику мусора удалять управляемые ресурсы, ассоциированные с данным экземпляром объекта. При вызове метода Dispose мы их освободим сами, вместе с неуправляемыми ресурсами. Деструктором удаляются только неуправляемые ресурсы, т.е. удаление управляемых, если они не были освобождены, поручается сборщику мусора. Теперь можно использовать экземпляры данного класса в блоках управления ресурсами:
//Неизвестно, когда ресурсы будут освобождены
MyResource res1 = new MyResource();
//Ресурсы будут освобождены после выхода из блока using using (MyResource res2 = new MyResource()) { }
346

5. СПЕЦИАЛЬНЫЕ ВОЗМОЖНОСТИ
Рассмотрим кратко некоторые дополнительные возможности, предо-
ставляемые .NET Framework.
§ 5.1. Универсальные типы
Универсальные члены (или универсальные шаблоны, generic members) – это некоторый аналог механизма шаблонов языка C++. Универсальными членами могут являться классы, структуры, интерфейсы, делегаты (универсальные типы), а также методы, свойства, индексаторы (универсальные методы), которые имеют прототипы (параметры типов) для одного или нескольких типов, которые они хранят или используют. Класс универсальной коллекции может использовать параметр типа в качестве типа объектов, которые в нем хранятся. Параметры типа отображаются как типы его полей и типы параметров его методов. Универсальный метод может использовать параметр типа как тип своего возвращаемого значения или как тип одного из своих формальных параметров.
В противоположность универсальным, будем называть не имеющие параметров типа члены конкретными членами.
5.1.1. Параметры типа
При описании синтаксиса типов и их членов мы использовали обозначения «параметры типа» и «ограничения параметров типа». Разберемся сначала с параметрами типа:
<параметры типа> :: "<" <формальный список типов> ">" <формальный список типов> :: <идентификатор> [,...]
Таким образом, указание параметров типа заключается в перечислении в угловых скобках одного или более идентификаторов, разделенных запятой. Эти идентификаторы являются формальным списком типов. Для типов с однобуквенными параметрами рекомендуется использовать «T» в качестве имени параметра типа (если должно быть несколько однобуквенных параметров, можно использовать следующие буквы алфавита – «U», «V», …). К описательным именам параметров типа рекомендуется добавлять префикс
«T» (например, TElement, TValue и т.д.).
После этого в данном универсальном типе или методе можно исполь-
347

зовать эти идентификаторы в качестве типов данных. Если тип является универсальным, то его параметры типа могут использовать все члены объекта, в том числе вложенные типы.
Пример: Samples\5.1\5_1_1_generic.
5.1.1.1. Использование универсальных типов
Если мы описали универсальный тип, то теперь его имя обязательно должно включать фактический список типов:
<имя универсального типа> :: <имя типа> "<"<фактический список типов>">" <фактический список типов> :: <тип> [, ...]
В качестве фактических типов могут выступать стандартные типы данных, ранее объявленные типы (делегаты, перечисления, классы, структуры и т.д.) или, если тип вложен в другой универсальный тип, то можно использовать параметры вмещающего универсального типа. Если фактические типы, соответствующие всем формальным типам, не указаны, или указаны неизвестные типы, возникает ошибка компиляции.
Если мы хотим использовать универсальный метод (универсального или обычного типа), то синтаксис будет следующим:
<вызов статического универсального метода> ::
<имя универсального типа>.<вызов универсального метода> <имя конкретного типа>.<вызов универсального метода>
<вызов универсального метода экземпляра> ::
<имя экземпляра>.<вызов универсального метода>
<вызов универсального метода> :: <имя метода> ["<" <фактический список типов> ">"] ([<фактические параметры вызова>]);
Фактический список типов метода указывать не обязательно, если его можно определить по типам фактических параметров вызова.
Пример:
class MyClass1<int> { } // Ошибка class MyClass2<T, U<V>> { } // Ошибка
class MyClass<T>
{
private T field;
public delegate void NotifyEvent(MyClass<T> sender); public event NotifyEvent OnChange;
public MyClass(T value)
{
field = value;
348

}
public T Prop
{
get { return field; } set
{
field = value;
if (OnChange != null) OnChange(this);
}
}
public override string ToString()
{
return field.ToString();
}
public bool EqualTo<U>(U value)
{
return field.Equals(value);
}
public static bool EqualTo<U>(T value1, U value2)
{
return value1.Equals(value2);
}
}
class Program
{
static void Out<T1, T2>(T1 var1, T2 var2)
{
Console.WriteLine("{0} & {1}", var1.GetType(),
var2.GetType());
}
static int Main()
{
MyClass<int> a = new MyClass<int>(5); Console.WriteLine(a); // 5 Console.WriteLine(a.EqualTo(5)); // True Console.WriteLine(MyClass.EqualTo(1, 1)); // Ошибка Console.WriteLine(MyClass<int>.EqualTo(1, 1.0)); // False Console.WriteLine(a.EqualTo<float>(5)); // False Console.WriteLine();
MyClass<string> s = new MyClass<string>("!!!"); MyClass<MyClass<string>> s2 = new
MyClass<MyClass<string>>(s); Console.WriteLine(s2); // !!!
MyClass<string>.NotifyEvent n = delegate(MyClass<string>
cls) {
Console.WriteLine("Значение изменилось на " +
cls.Prop);
};
a.OnChange += n; // Ошибка s.OnChange += n;
s.Prop = "???"; // Значение изменилось на ???
349

Console.WriteLine();
Out(1, 1.5); // System.Int32 & System.Double Out<double, double>(1, 1.5); // System.Double &
System.Double
return 0;
}
}
Ошибки:
1)int является типом, а не идентификатором;
2)U<V> не является идентификатором;
3)Имя универсального класса использовано без фактического списка параметров типа;
4)MyClass<string>.NotifyEvent и MyClass<int>.NotifyEvent – разные типы делегатов.
Как видно, у класса фактические параметры типа должны быть указаны всегда, а у методов – нет, если аргументы этих типов используются в списке параметров вызова. В этом случае происходит процесс, называемый выводом типов. Например, при первом вызове метода Out параметры имеют типы int
иdouble. Если нас устраивает, что T1 = int и T2 = double, то фактические параметры типа указывать не обязательно. Во втором случае мы указали их явно.
5.1.1.2. Отличие от шаблонов языка C++
По сравнению с шаблонами языка C++, универсальные члены имеют ряд ограничений. В частности:
• Нельзя использовать параметр типа в качестве базового класса:
class MyClass3<T> : T { } // Ошибка
• Нельзя использовать не являющиеся типами параметры шаблона:
class MyClass4<int N> { } // Ошибка
Но самое серьезное отличие состоит в том, что язык C++ позволяет использование кода, который может быть допустимым не для всех параметров типа в шаблоне, и который затем проверяется для конкретного типа, используемого в качестве параметра. Например:
template <class T> class MyClass
{
public:
MyClass(T value)
350