Допустим, мы хотим, чтобы класс Bank хранил набор счетов, представленных объектами некоторого класса Account:
class Account
{
public int Id { get; private set;} // номер счета
public Account(int _id)
{
Id = _id;
}
}
Но у этого класса может быть много наследников: DepositAccount (депозитный счет), DemandAccount (счет до востребования) и т.д. Однако мы не можем знать, какой вид счета в банке в данном случае будет использоваться. Возможно, банк будет принимать только депозитные счета. И в этом случае в качестве универсального параметра можно установить тип Account:
class Bank<T> where T: Account
{
T[] accounts;
public Bank(T[] accs)
{
this.accounts = accs;
}
// вывод информации обо всех аккаунтах
public void AccountsInfo()
{
foreach(Account acc in accounts)
{
Console.WriteLine(acc.Id);
}
}
}
С помощью выражения where T: Account мы указываем, что используемый тип T обязательно должен быть классом Account или его наследником.
Теперь применим класс Bank в программе:
class Program
{
static void Main(string[] args)
{
Account[] accounts = new Account[]
{
new Account(1857), new Account(2225), new Account(33232)
};
Bank<Account> bank = new Bank<Account>(accounts);
bank.AccountsInfo();
Console.ReadLine();
}
}
При этом мы можем задать множество ограничений через запятую:
class Client { }
interface IAccount { }
interface ITransfer { }
class Bank<T> where T: Client, IAccount, ITransfer
{}
Следует учитывать, что в данном случае только один класс может использоваться в качестве ограничения, в то время как интерфейсов может применяться несколько. При этом класс должен указываться первым.
Кроме того, можно указать ограничение, чтобы использовались только структуры:
class Bank<T> where T: struct
{}
или классы:
class Bank<T> where T: class
{}
А также можно задать в качестве ограничения класс или структуру, которые реализуют конструктор по умолчанию с помощью слова new:
class Bank<T> where T: new()
{}
Использование нескольких универсальных параметров
Мы можем также задать сразу несколько универсальных параметров и ограничения к каждому из них:
class Operation<T, U>
where U: class
where T: Account, new() { }
Обобщенные методы
Кроме обобщенных классов можно также создавать обобщенные методы, которые точно также будут использовать универсальные параметры. Например:
class Program
{
static void Main(string[] args)
{
Client client2 = new Client("Том", "Симпсон");
Display<Client>(client2);
Console.ReadLine();
}
private static void Display<T>(T person) where T: Person
{
person.Display();
}
}
abstract class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Person(string lName, string fName)
{
FirstName = fName;
LastName = lName;
}
public abstract void Display();
}
class Client: Person
{
public Client(string lName, string fName)
: base(fName, lName)
{
}
public override void Display()
{
Console.WriteLine(FirstName + " " + LastName);
}
}
Наследование обобщенных типов
Один обобщенный класс может быть унаследован от другого обобщенного:
class Transaction<T>
{
T inAccount;
T outAccount;
public Transaction(T inAcc, T outAcc)
{
inAccount = inAcc;
outAccount = outAcc;
}
}
class UniversalTransaction<T>: Transaction<T>
{
public UniversalTransaction(T inAcc, T outAcc): base(inAcc, outAcc)
{
}
}
При этом производный класс необязательно должен быть обобщенным. Например:
class StringTransaction: Transaction<string>
{
public StringTransaction(string inAcc, string outAcc)
: base(inAcc, outAcc)
{
}
}
Теперь в производном классе в качестве типа будет использоваться тип string
Также производный класс может быть типизирован параметром совсем другого типа, отличного от типа базового класса:
class IntTransaction<T>: Transaction<int>
{
T operationCode = default(T);
public IntTransaction(int inAcc, int outAcc, T operCode)
: base(inAcc, outAcc)
{
this.operationCode = operCode;
}
}