Преобразования и операции внутри обобщенных типов

Преобразование от одного типа к другому или применение операций к параметризованным типам внутри обобщений — неизбежно непростая задача. Чтобы проиллюстрировать это, давайте разработаем обобщенную структуру Complex, представляющую комплексное число.

Предположим, что необходимо иметь возможность указывать тип значения, используемый внутри этого класса для представления реальной и мнимой частей комплексного числа.

Этот пример довольно надуманный, поскольку обычно вполне достаточно представлять компоненты комплексного числа чем-нибудь вроде System.Double. Однако для примера давайте примем, что компоненты должны быть представлены с помощью типа System.Int64. Во время обсуждения, чтобы не загромождать пример и сосредоточиться на проблемах, касающихся обобщений, все канонические конструкции, которые должна реализовать обобщенная структура Complex, будут игнорироваться.

Начать можно со следующей реализации структуры Complex:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
public struct Complex<T> where T: struct
{
public Complex ( T real, T imaginary )  { 
this.real = real; 
this.imaginary = imaginary;
}
public Т Real {
get { return real; 
} 
set { 
real = value; }
}
public T Img {
get { 
return imaginary; 
} 
set { 
imaginary = value; }
}
private T real; 
private T imaginary;
}
public class EntryPoint {
static void Main()  { 
Complex<Int64> с =
new Complex<Int64> ( 4, 5 );
}

Это хорошее начало, но давайте сделаем этот тип значений несколько более полезным. Можно получить выигрыш от наличия свойства Magnitude, которое возвращает квадратный корень из результата умножения двух компонентов. Попробуем добавить это свойство:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System; 
public struct Complex<T> where T: struct
{
public Complex ( T real, T imaginary )  { 
this.real = real; 
this.imaginary = imaginary;
}
public T Real {
get { return real; } 
set { real = value; }
}
public T Img {
get { return imaginary; } 
set { imaginary = value; }
}
public T Magnitude { 
get {
//HE КОМПИЛИРУЕТСЯ!!!
return Math.Sqrt( real * real + imaginary * imaginary );
}
private T real; 
private T imaginary;
}
public class EntryPoint {
static void Main()  {
Complex<Int64> с = new Complex<Int64> ( 3, 4 ); 
Console.WriteLine( "Magnitude is {0}", c.Magnitude );
}

Если вы попытаетесь скомпилировать этот код, то наверняка удивитесь, получив следующую ошибку компиляции:
error CS0019: Operator cannot be applied to operands of type ‘T’ and fTf

ошибка CS0019: Операция * не может быть применена к операндам типов Т и Т

Это блестящий пример проблемы с использованием операций в обобщенном коде. Ошибка во время компиляции порождена необходимостью компилировать обобщенный код обобщенным способом, с учетом того, что конструируемые типы формируются во время выполнения и могут быть получены из типов значений, не поддерживающих операцию.

В этом случае компилятор никак не может узнать, сумеет ли тип, подставленный вместо Т в конструируемом типе, когда-то в будущем поддержать операцию умножения. Что же делать? Распространенный прием заключается в том, чтобы вынести эту операцию за пределы определения Complex. Подходящим инструментом для этого может быть делегат.

Рассмотрим пример Complex, в котором так и сделано:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
public struct Complex<T>
where T: struct, IConvertible
{
// Делегат для выполнения умножения.
public delegate T BinaryOp( T vail, T val2 );
public Complex( T real,
T imaginary,
BinaryOp mult,
BinaryOp add,
Converter<double, T> convToT )  { 
this.real = real; 
this.imaginary = imaginary; 
this.mult = mult; 
this.add = add; 
this.convToT = convToT;
}
public T Real {
get { return real;  } 
set { real = value;  }
}
public T Img {
get { return imaginary; } 
set { imaginary = value; }
}
public T Magnitude { 
get {
double magnitude =
Math.Sqrt( Convert.ToDouble(add(mult(real, real), mult(imaginary, imaginary))) ) ; 
return convToT ( magnitude ) ;
}
private T real; 
private T imaginary; 
private BinaryOp mult; 
private BinaryOp add;
private Converter<double, T> convToT;
public class EntryPoint {
static void Main()  { 
Complex<Int64> с =
new Complex<Int64> ( 3, 4, EntryPoint.Multiplylnt64, EntryPoint.Addlnt64, EntryPoint.DoubleToInt64 ); 
Console.WriteLine( "Модуль равен {0}", с.Magnitude );
}
static Int64 Multiplylnt64( Int64 vail, Int64 val2 )  { 
return vail * val2;
}
static Int64 Addlnt64( Int64 vail, Int64 val2 )  { 
return vail + val2;
}
static Int64 DoubleToInt64 ( double d )  { 
return Convert.ToInt64 ( d );
}

Возможно, глядя на этот код, вы недоумеваете, что пошло не так, и почему настолько возросла сложность, когда понадобилось всего лишь найти выдуманное определение модуля комплексного числа. Как уже упоминалось, пришлось ввести делегат для внешней по отношению к обобщенному типу обработки операции умножения. Делегат определен как Complex.Multiply. Конструктор Complex должен получить третий параметр, ссылающийся на метод, который будет использовать делегат для выполнения умножения. В данном случае умножение выполняет EntryPoint.Multiplylnt64.

Таким образом, когда свойству Magnitude нужно перемножить компоненты, оно должно использовать делегат вместо обычной операции умножения.

Естественно, при вызове делегата в конечном итоге он сведет все к операции умножения. Однако применение операции теперь вынесено за пределы обобщенного типа Complex. Несложно заметить, что аналогичная техника применяется и для операции сложения.

Несомненно, вы заметили усложнение методов доступа свойства. Метод Math.Sqrt принимает тип System.Double. Это объясняет вызов метода Convert.ToDouble. Чтобы все проходило гладко, к Т добавлено ограничение, чтобы этот тип обеспечивал поддержку IConvertible. Но это еще не все. Метод Math.Sqrt возвращает System.Double, и этот тип значения должен быть преобразован обратно в Т.

При этом нельзя полагаться на класс System.Convert, потому что на момент компиляции неизвестно, какой тип придется преобразовывать. Опять-таки, эту операцию преобразования понадобится вынести наружу. Именно для таких ситуаций в .NET Framework предусмотрен делегат Converter нуждается в делегате преобразования Converter.

Во время конструирования потребуется передать метод, который будет вызываться через этот делегат, и в данном случае таким методом является EntryPoint.DoubleToInt64. После всего этого свойство Complex.Magnitude работает, как ожидалось, хотя и не без некоторых дополнительных усилий.

Сложность использования Complex, как показано в предыдущем примере, в значительной мере можно сократить с помощью лямбда-выражений. Применяя лямбда-выражения, удается полностью исключить необходимость в определении методов операций, таких как Multiply Int 64, Addlnt 64 и DoubleToInt64.

Теперь предположим, что экземпляры Complex будут использоваться в качестве ключевых значений в обобщенном типе SortedList. Для этого тип Complex должен реализовать IComparable. Посмотрим, что понадобится сделать, чтобы это стало возможным.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
public struct Complex<T> : IComparable<Complex<T> > where T: struct, IConvertible, IComparable
{
// Делегат для выполнения умножения.
public delegate T BinaryOp( T vail, T val2 );
public Complex( T real, T imaginary,
BinaryOp mult,
BinaryOp add,
Converter<double, T> convToT )  { 
this.real = real; 
this.imaginary = imaginary; 
this.mult = mult; 
this.add = add; 
this.convToT = convToT;
}
public T Real {
get { return real; } 
set { real = value; }
}
public T Img {
get { return imaginary; } 
set { imaginary = value; }
}
public T Magnitude { 
get {
double magnitude =
Math.Sqrt( Convert.ToDouble(add(mult(real, real),
mult(imaginary, imaginary))) ) ;
return convToT( magnitude ) ;
}
public int CompareTo( Complex<T> other ) {
return Magnitude. CompareTo ( other .Magnitude ) ;
}
private T real;
private T imaginary;
private BinaryOp mult;
private BinaryOp add;
private Converter<double, T> convToT;
}
public class EntryPoint {
static void Main()  { 
Complex<Int64> с =
new Complex<Int64>( 3, 4,
EntryPoint.MultiplyInt64,
EntryPoint.Addlnt64, EntryPoint.DoubleToInt64 )
Console.WriteLine( "Модуль равен {0}", с.Magnitude );
}
static Int64 Multiplylnt64( Int64 vail, Int64 val2 )  
{ 
return vail * val2;
}
static Int64 Addlnt64( Int64 vail, Int64 val2 )  
{ 
return vail + val2;
}
static Int64 DoubleToInt64 ( double d )  
{ 
return Convert.ToInt64 ( d );
}

В приведенной реализации интерфейса IComparable> два типа Complex рассматриваются как эквивалентные, если у них одинаковое значение модуля. Поэтому большая часть необходимой работы для сравнения уже выполнена. Однако вместо того, чтобы положиться на операцию неравенства языка С#, опять нужно применить механизм, не использующий операций. В данном случае был вызван метод CompareTo.

Разумеется, это потребовало наложения еще одного ограничения на тип Т: он должен поддерживать необобщенный интерфейс IComparable, потому что тип, предоставленный для Т, может быть даже и необобщенным, т.е. поддерживать только IComparable, а не IComparable.

Еще одна вещь, которую стоит упомянуть: предыдущее ограничение по необобщенному интерфейсу IComparable несколько затрудняет для Complex хранение обобщенных структур, поскольку обобщенная структура может реализовывать вместо этого IComparable. Фактически, при текущем определении невозможно указать тип Complex конструировался из типов, которые могут реализовывать IComparable, или IComparable, или даже оба сразу.

Давайте посмотрим, как это сделать.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
public struct Complex<T> : IComparable<Complex<T> > where T: struct
{
// Делегат для выполнения умножения.
public delegate T BinaryOp( T vail, T val2 );
public Complex( T real, T imaginary,
BinaryOp mult,
BinaryOp add,
Converter<double, T> convToT )  { 
this.real = real; 
this.imaginary = imaginary; 
this.mult = mult; 
this.add = add; 
this.convToT = convToT;
}
public T Real {
get { return real; } 
set { real = value; }
}
public T Img {
get { return imaginary; } 
set { imaginary = value; }
public Т Magnitude { 
get {
double magnitude =
Math.Sqrt ( Convert.ToDouble(add(mult (real, real), mult(imaginary, imaginary))) ) ; 
return convToT ( magnitude );
}
public int CompareTo ( Complex<T> other )  {
return Comparer<T>.Default.Compare( this.Magnitude, other.Magnitude );
}
private T real;
private T imaginary;
private BinaryOp mult;
private BinaryOp add;
private Converter<double, T> convToT;
}
public class EntryPoint {
static void Main()  { 
Complex<Int64> с =
new Complex<Int64>( 3, 4,
EntryPoint.MultiplyInt64, EntryPoint.Addlnt64, EntryPoint.DoubleToInt64 ); 
Console.WriteLine ( "Модуль равен {0}", с.Magnitude );
}
static void DummyMethod ( Complex<Complex<int> > с ) { }
static Int64 Addlnt64( Int64 vail, Int64 val2 )  { 
return vail + val2;
}
static Int64 Multiplylnt64 ( Int64 vail, Int64 val2 )  { 
return vail * val2;
}
static Int64 DoubleToInt64 ( double d )  { 
return Convert.ToInt64 ( d );
}

В этом примере было удалено ограничение на Т, требующее реализации интерфейса IComparable. Вместо этого метод CompareTo полагается на обобщенный компаратор по умолчанию, определенный в пространстве имен System.Collections.Generic.

Класс обобщенного компаратора Comparer вводит еще один промежуточный уровень в форме класса для сравнения двух экземпляров. По сути, он выносит наружу возможность сравнения двух экземпляров. Если необходима пользовательская реализация IComparer, потребуется выполнять наследование от Comparer.

Чтобы скомпилировался метод DummyMethod, дополнительно пришлось удалить ограничение IConvertible на Т. Это объясняется тем, что Complex не реализует IConvertible, и когда Т заменяется Complex (тем самым формируя Complex), то в результате Т не реализует IConvertible.

При создании обобщенных типов старайтесь не накладывать слишком много ограничений на содержащиеся в коллекции типы. Например, не требуйте от всех содержащихся типов реализации интерфейса IConvertible. Часто такие ограничения можно вынести наружу, используя вспомогательный объект в сочетании с делегатом.

Задумаемся на минуту об удалении этого ограничения. В свойстве Magnitude вызывался метод Convert. ToDouble. Однако, поскольку ограничение удалено, существует возможность получения исключения времени выполнения, например, когда тип, представленный Т, не реализует IConvertible.

Поскольку обобщения предназначены для обеспечения более высокой безопасности типов и помогают избежать исключений времени выполнения, должен существовать лучший способ. Фактически он есть, и будет гораздо лучше, если предоставить Complex еще один конвертер в форме делегата Convert в конструкторе, как показано ниже:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
using System;
using System.Collections.Generic;
public struct Complex<T> : IComparable<Complex<T> > where T: struct
{
// Делегат для выполнения умножения.
public delegate T BinaryOp( T vail, T val2 );
public Complex( T real, T imaginary,
BinaryOp mult,
BinaryOp add,
Converter<T, double> convToDouble,
Converter<double, T> convToT )  {
this, real = real; 
this.imaginary = imaginary; 
this.mult = mult; this.add = add;
this.convToDouble = convToDouble;
this.convToT = convToT;
}
public T Real {
get { return real; } 
set { real = value;  }
}
public T Img {
get { return imaginary; } 
set { imaginary = value; }
}
public T Magnitude { 
get {
double magnitude =
Math.Sqrt( convToDouble(add(mult(real, real),
mult(imaginary, imaginary))) ); 
return convToT( magnitude ) ;
}
public int CompareTo( Complex<T> other )  {
return Comparer<T>.Default.Compare ( 
this.Magnitude, other.Magnitude );
}
private T real; 
private T imaginary;
private BinaryOp mult; private BinaryOp add;
private Converter<T, double> convToDouble;
private Converter<double, T> convToT;
}
public class EntryPoint {
static void Main()  { 
Complex<Int64> с =
new Complex<Int64> ( 3, 4,
EntryPoint.MultiplyInt64, EntryPoint.Addlnt64, EntryPoint.Int64ToDouble,
EntryPoint.DoubleToInt64 ); 
Console.WriteLine( "Модуль равен {0}", с.Magnitude ) ;
}
static void DummyMethod( Complex<Complex<int> > с )  
{ 
}
static Int64 Multiplylnt64( Int64 vail, Int64 val2 )  { 
return vail * val2;
}
static Int64 Addlnt64( Int64 vail, Int64 val2 )  { 
return vail + val2;
}
static Int64 DoubleToInt64 ( double d )  { 
return Convert.ToInt64 ( d );
}
static double Int64ToDouble( Int64 i ) { 
return Convert.ToDouble( i ) ;
}

Теперь тип Complex может содержать любые структуры, независимо от того, обобщенные они или нет. Однако его потребуется оснастить необходимыми средствами для преобразований в тип double и обратно, а также операцией умножения и компонентными типами. Эта структура Complex не может служить руководством по созданию представлений комплексных чисел. На самом деле это просто довольно надуманный пример, предназначенный для того, чтобы проиллюстрировать многие нюансы, с которыми придется иметь дело при создании эффективных обобщенных типов.

С примерами применения некоторых из описанных приемов придется столкнуться при работе с обобщенными контейнерами, представленными в библиотеке BCL.

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100