Сегодня хочется рассказать о том, как описать в иерархии типов возможность (интерфейс) клонирования объектов. Подразумевается, что программист уже знает, каким именно образом будет реализован код клонирования (тут вариантов много и выбор конкретного зависит от очень большого числа требований и возможностей) и речь пойдёт о том, как бы с максимальным удобством для пользователя кода всё оформить.
Обычно встречается следующая реализация:
{
public abstract object Clone();
}
public class Derived : Base
{
public override object Clone() {
// <Clone implementation>
return new Derived();
}
}
или
{
public abstract Base Clone();
}
public class Derived : Base
{
public override Base Clone() {
// <Clone implementation>
return new Derived();
}
}
Во-первых, реализация ICloneable является моветоном и в хороших API встречаться не должна (и вот почему). Во-вторых, неудобно здесь то, что Derived::Clone() возвращает совсем не Derived и, зачастую, к результату придётся применять операцию приведения типа (type cast). Для того, что бы таких неудобств ни возникало, достаточно оформить код классов следующим образом:
{
protected abstract Base CloneBase();
public Base Clone() {
return CloneBase();
}
}
public sealed class Derived : Base
{
protected override Base CloneBase() {
return Clone();
}
public new Derived Clone() {
// <Clone implementation>
return new Derived();
}
}
То есть, в базе "не типизированный" Clone() делается защищённым и переименовывается, а "снаружи" для него добавляется невиртуальный открытый метод Clone(), вызывающий защищённую реализацию. В наследниках наоборот: сама реализация клонирования пишется в открытом не виртуальном методе, который имеет удобный (нужный пользователю) и максимально точный тип возвращаемого значения и в переопределении метода базового типа вызывается именно этот метод. ICloneable не используется.
Общее правило такое: в классе, у которого могут быть наследники, объявляется два метода, каждый из которых возвращает "текущий" тип: защищённый (protected) виртуальный с реализацией клонирования текущего типа (или защищённый абстрактный, если класс абстрактный) и открытый, который возвращает вызов виртуального (абстрактного). В классе, у которго нет наследников тоже делается два метода: возвращающий "текущий" тип открытый (public) с реализацией клонирования и перегрузка (overriding) метода базового класса, возвращающая вызов открытого метода.
Рассмотрим пример ещё раз, в параллель с правилом:
public abstract class Base
{
// …объявляется два метода, каждый из которых возвращает "текущий" тип - "Base":
// …защищённый виртуальный с реализацией клонирования текущего типа
// (или защищённый абстрактный, если класс абстрактный)…
protected abstract Base CloneBase();
// …и открытый, …
public Base Clone() {
// …который возвращает вызов виртуального (абстрактного).
return CloneBase();
}
}
// В классе, у которго нет наследников…
public sealed class Derived : Base
{
// …тоже делается два метода:
// …возвращающий "текущий" тип открытый…
public new Derived Clone() {
// …с реализацией клонирования
return new Derived();
}
// …и перегрузка метода базового класса, …
protected override Base CloneBase() {
// …возвращающая вызов открытого метода.
return Clone();
}
}
(В коде выше я переставил местами объявления методов класса Derived для того, что бы они соответствовали формулировке правила.)
Посмотрим, зачем же все эти сложности:
static Derived GetDerived() { return /* Some Derived */; }
static void Main() {
Base b = GetBase();
Base b2 = b.Clone();
Derived d = GetDerived();
Derived d2 = d.Clone();
}
То есть мы статически (на этапе компиляции и даже на этапе написания кода) имеем максимально точный тип и избавлены от необходимости пользоваться приведениями типов там, где это, очевидно, не нужно, то есть получаем более чистый, легче читаемый код вместе с лучшей поддержкой средств разработки (IDE) и анализа.
Бывают случаи и запутаннее, когда иерархия более глубокая, и между Base и Derived возникают различные Middle. Тогда описанное выше правило работает точно так же, с небольшим дополнением: не корневой класс в иерархии клонирования, у которого так же будут (могут быть) наследники должен написать запечатанную (sealed) перегрузку метода клонирования базового класса, из которой вернуть результат объявленного в нём виртуального метода:
// у которого так же будут (могут быть) наследники…
public class Middle : Base
{
// …должен написать запечатанную перегрузку метода клонирования
// базового класса, …
protected sealed override Base CloneBase() {
// из которой вернуть результат объявленного в нём виртуального метода.
return CloneMiddle();
}
protected virtual Middle CloneMiddle() {
// <Clone implementation>
return new Middle();
}
public new Middle Clone() {
return CloneMiddle();
}
}
Осталось рассмотреть последний случай, когда такой middle-класс является абстрактным. В соответствии с описанными правилами, его объявление будет таким:
{
protected sealed override Middle CloneMiddle() {
return CloneMiddle2();
}
protected abstract Middle2 CloneMiddle2(); {
public new Middle2 Clone() {
return CloneMiddle2();
}
}
А "листовой" (не имеющий своих наследников) наследник уже этого класса будет таким:
{
protected override Middle2 CloneMiddle2() {
return Clone();
}
public new Derived2 Clone() {
// <Clone implementation>
return new Derived2();
}
}
Да, конечно, кода получается несколько больше, чем вы, возможно, привыкли. Но код, сравнительно, используется намного чаще, чем пишется и читается намного чаще, чем используется. Показанный здесь "синтаксический оверхед" вызван невозможностью в C# применять ко-/контр-вариантность к типам возвращаемого значения и параметрам при перегрузке методов, а так же желанием иметь более дружественные в использовании классы.
Комментариев нет:
Отправить комментарий