Czy istnieje jakiś szczególny powód, dla którego ICloneable<T>
nie istnieje rodzajowy ?
Byłoby o wiele wygodniej, gdybym nie musiał go rzucać za każdym razem, gdy coś klonuję.
Czy istnieje jakiś szczególny powód, dla którego ICloneable<T>
nie istnieje rodzajowy ?
Byłoby o wiele wygodniej, gdybym nie musiał go rzucać za każdym razem, gdy coś klonuję.
Odpowiedzi:
ICloneable jest obecnie uważany za zły interfejs API, ponieważ nie określa, czy wynikiem jest głęboka, czy płytka kopia. Myślę, że właśnie dlatego nie poprawiają tego interfejsu.
Prawdopodobnie możesz wykonać typową metodę rozszerzania klonowania, ale myślę, że wymagałaby ona innej nazwy, ponieważ metody rozszerzenia mają mniejszy priorytet niż oryginalne.
List<T>
miał metodę klonowania, spodziewałbym się, że przyniesie ona pozycję, List<T>
której elementy mają takie same tożsamości jak te z oryginalnej listy, ale oczekiwałbym, że wszelkie wewnętrzne struktury danych zostaną w razie potrzeby zduplikowane, aby zapewnić, że nic zrobione na jednej liście nie wpłynie na że tożsamość przedmiotów przechowywanych w drugiej. Gdzie jest dwuznaczność? Większy problem z klonowaniem wyposażony w odmianie „problem diament”: jeśli CloneableFoo
dziedziczy z [nie publicznie Cloneable] Foo
, powinny CloneableDerivedFoo
pochodzić z ...
identity
na samej liście, na przykład (w przypadku listy list)? Jednak ignorując to, twoje oczekiwania nie są jedynym możliwym pomysłem, jaki ludzie mogą mieć, dzwoniąc lub wdrażając Clone
. Co jeśli autorzy bibliotek wdrażający inną listę nie spełnią twoich oczekiwań? Interfejs API powinien być trywialnie jednoznaczny, a nie prawdopodobnie jednoznaczny.
Clone
stworzy klasę, która wykonuje głębokie klonowanie i wywoła wszystkie jego części, nie będzie działać przewidywalnie - w zależności od tego, czy ta część została zaimplementowana przez ciebie, czy przez tę osobę kto lubi głębokie klonowanie. twoje zdanie na temat wzorców jest poprawne, ale posiadanie IMHO w API nie jest wystarczająco jasne - albo należy je wywołać, ShallowCopy
aby podkreślić punkt, albo wcale.
Oprócz odpowiedzi Andreya (z którą się zgadzam, +1) - kiedy ICloneable
to zrobisz, możesz także wybrać jawną implementację, aby publiczny Clone()
zwrot był typowanym obiektem:
public Foo Clone() { /* your code */ }
object ICloneable.Clone() {return Clone();}
Oczywiście istnieje druga kwestia z ICloneable<T>
dziedziczeniem rodzajowym .
Jeżeli mam:
public class Foo {}
public class Bar : Foo {}
I wdrożyłem ICloneable<T>
, a następnie czy wdrożę ICloneable<Foo>
? ICloneable<Bar>
? Szybko zaczynasz wdrażać wiele identycznych interfejsów ... Porównaj z obsadą ... i czy to naprawdę takie złe?
Muszę zapytać, co dokładnie zrobiłbyś z interfejsem innym niż wdrożenie? Interfejsy są zwykle użyteczne tylko wtedy, gdy się na nie rzucisz (tj. Czy ta klasa obsługuje „IBar”), lub mają parametry lub ustawiające parametry, które je przyjmują (tzn. Biorę „IBar”). Dzięki ICloneable - przeszliśmy przez cały Framework i nie udało nam się znaleźć żadnego użycia w innym miejscu, które byłoby czymś innym niż jego implementacja. Nie udało nam się również znaleźć żadnego zastosowania w „prawdziwym świecie”, które również robi coś innego niż wdrożenie (w ~ 60 000 aplikacji, do których mamy dostęp).
Teraz, jeśli chcesz wymusić wzorzec, który chcesz wdrożyć w swoich „klonowalnych” obiektach, jest to całkiem dobre zastosowanie - i śmiało. Możesz także zdecydować, co dokładnie oznacza dla ciebie „klonowanie” (tj. Głębokie lub płytkie). Jednak w takim przypadku nie musimy go (BCL) definiować. Abstrakcje definiujemy w BCL tylko wtedy, gdy zachodzi potrzeba wymiany instancji typowanych jako abstrakcja między niepowiązanymi bibliotekami.
David Kean (zespół BCL)
ICloneable<out T>
może być całkiem przydatna, jeśli zostanie odziedziczona ISelf<out T>
za pomocą jednej metody Self
typu T
. Często nie potrzeba „czegoś, co można klonować”, ale równie dobrze może potrzebować czegoś, co można T
klonować. Jeśli implementuje się klonowalny obiekt ISelf<itsOwnType>
, procedura, która wymaga T
klonowalnego, może zaakceptować parametr typu ICloneable<T>
, nawet jeśli nie wszystkie klonowalne pochodne tego samego T
przodka.
ICloneable<T>
, chociaż szersza struktura utrzymywania równoległych klas mutable i niezmiennych może być bardziej pomocna. Innymi słowy, kod, który musi zobaczyć, co Foo
zawiera jakiś rodzaj, ale nie będzie go mutować ani oczekiwać, że nigdy się nie zmieni, mógłby użyć IReadableFoo
, podczas gdy ...
Foo
może użyć ImmutableFoo
kodu while, który chce nim manipulować, może użyć MutableFoo
. Podany kod dowolnego typu IReadableFoo
powinien być w stanie uzyskać wersję zmienną lub niezmienną. Taki framework byłby fajny, ale niestety nie mogę znaleźć żadnego fajnego sposobu na skonfigurowanie rzeczy w ogólny sposób. Gdyby istniał spójny sposób na utworzenie opakowania tylko do odczytu dla klasy, można by tego użyć w połączeniu z ICloneable<T>
niezmienną kopią klasy, która przechowuje T
„.
List<T>
, tak, że sklonowana List<T>
jest nowa kolekcja zawierająca wskaźniki do wszystkich tych samych obiektów w oryginalnej kolekcji, istnieją dwa proste sposoby, aby to zrobić bez ICloneable<T>
. Pierwsza to Enumerable.ToList()
metoda rozszerzenia: List<foo> clone = original.ToList();
druga to List<T>
konstruktor, który przyjmuje IEnumerable<T>
: List<foo> clone = new List<foo>(original);
Podejrzewam, że metoda rozszerzenia prawdopodobnie po prostu wywołuje konstruktor, ale oba z nich wykonają to, o co prosisz. ;)
Myślę, że pytanie „dlaczego” jest niepotrzebne. Istnieje wiele interfejsów / klas / etc ... co jest bardzo przydatne, ale nie jest częścią podstawowej biblioteki Frameworku .NET.
Ale głównie możesz to zrobić sam.
public interface ICloneable<T> : ICloneable {
new T Clone();
}
public abstract class CloneableBase<T> : ICloneable<T> where T : CloneableBase<T> {
public abstract T Clone();
object ICloneable.Clone() { return this.Clone(); }
}
public abstract class CloneableExBase<T> : CloneableBase<T> where T : CloneableExBase<T> {
protected abstract T CreateClone();
protected abstract void FillClone( T clone );
public override T Clone() {
T clone = this.CreateClone();
if ( object.ReferenceEquals( clone, null ) ) { throw new NullReferenceException( "Clone was not created." ); }
return clone
}
}
public abstract class PersonBase<T> : CloneableExBase<T> where T : PersonBase<T> {
public string Name { get; set; }
protected override void FillClone( T clone ) {
clone.Name = this.Name;
}
}
public sealed class Person : PersonBase<Person> {
protected override Person CreateClone() { return new Person(); }
}
public abstract class EmployeeBase<T> : PersonBase<T> where T : EmployeeBase<T> {
public string Department { get; set; }
protected override void FillClone( T clone ) {
base.FillClone( clone );
clone.Department = this.Department;
}
}
public sealed class Employee : EmployeeBase<Employee> {
protected override Employee CreateClone() { return new Employee(); }
}
W razie potrzeby napisanie interfejsu jest bardzo proste :
public interface ICloneable<T> : ICloneable
where T : ICloneable<T>
{
new T Clone();
}
Czytając ostatnio artykuł Dlaczego kopiowanie obiektu jest straszne? Myślę, że to pytanie wymaga dodatkowego wyjaśnienia. Inne odpowiedzi tutaj zawierają dobre porady, ale odpowiedź nie jest kompletna - dlaczego nie ICloneable<T>
?
Stosowanie
Więc masz klasę, która to implementuje. Podczas gdy wcześniej miałeś metodę, która chciała ICloneable
, teraz musi być ogólna, aby ją zaakceptować ICloneable<T>
. Musisz go edytować.
Następnie możesz mieć metodę sprawdzającą, czy obiekt is ICloneable
. Co teraz? Nie możesz tego zrobić, is ICloneable<>
a ponieważ nie znasz typu obiektu w typie kompilacyjnym, nie możesz uczynić metody ogólną. Pierwszy prawdziwy problem.
Więc musisz mieć jedno ICloneable<T>
i drugie ICloneable
, wdrażające drugie. Dlatego implementator musiałby wdrożyć obie metody - object Clone()
i T Clone()
. Nie, dziękuję, mamy już dość zabawy IEnumerable
.
Jak już wspomniano, istnieje również złożoność dziedziczenia. Chociaż kowariancja może wydawać się rozwiązać ten problem, typ pochodny musi zaimplementować ICloneable<T>
swój własny typ, ale istnieje już metoda o tej samej sygnaturze (= parametry, w zasadzie) - Clone()
klasy podstawowej. Wyraźne określenie nowego interfejsu metody klonowania jest bezcelowe, stracisz przewagę, której szukałeś podczas tworzenia ICloneable<T>
. Dodaj new
słowo kluczowe. Ale nie zapominaj, że musiałbyś również przesłonić klasę podstawową ” Clone()
(implementacja musi pozostać jednolita dla wszystkich klas pochodnych, tj. Aby zwrócić ten sam obiekt z każdej metody klonowania, więc podstawową metodą klonowania musi być virtual
)! Ale niestety nie można zarówno override
inew
metody z tym samym podpisem. Wybierając pierwsze słowo kluczowe, stracisz cel, który chciałeś mieć podczas dodawania ICloneable<T>
. Wybierając drugi, zepsułbyś sam interfejs, tworząc metody, które powinny robić to samo, zwracając różne obiekty.
Punkt
Chcesz ICloneable<T>
wygody, ale komfort nie jest tym, do czego przeznaczone są interfejsy, ich znaczenie to (ogólnie OOP) ujednolicenie zachowania obiektów (chociaż w C # ogranicza się do ujednolicenia zachowania zewnętrznego, np. Metod i właściwości, a nie ich działania).
Jeśli pierwszy powód Cię jeszcze nie przekonał, możesz sprzeciwić się temu, że ICloneable<T>
może również działać restrykcyjnie, aby ograniczyć typ zwracany z metody klonowania. Jednak paskudny programista może implementować ICloneable<T>
tam, gdzie T nie jest typem, który go implementuje. Tak więc, aby osiągnąć swoje ograniczenie, możesz dodać ładne ograniczenie do parametru ogólnego:
public interface ICloneable<T> : ICloneable where T : ICloneable<T>
Z pewnością bardziej restrykcyjne niż ten bez where
, nadal nie możesz ograniczyć tego, że T jest typem implementującym interfejs (możesz wywodzić się z ICloneable<T>
innego typu który to implementuje).
Widzisz, nawet ten cel nie zostałby osiągnięty (oryginał ICloneable
również zawodzi, żaden interfejs nie może naprawdę ograniczyć zachowania klasy implementującej).
Jak widać, dowodzi to, że ogólny interfejs jest trudny do pełnego wdrożenia, a także naprawdę niepotrzebny i bezużyteczny.
Ale wracając do pytania, tak naprawdę szukasz komfortu podczas klonowania obiektu. Można to zrobić na dwa sposoby:
public class Base : ICloneable
{
public Base Clone()
{
return this.CloneImpl() as Base;
}
object ICloneable.Clone()
{
return this.CloneImpl();
}
protected virtual object CloneImpl()
{
return new Base();
}
}
public class Derived : Base
{
public new Derived Clone()
{
return this.CloneImpl() as Derived;
}
protected override object CloneImpl()
{
return new Derived();
}
}
To rozwiązanie zapewnia zarówno wygodę, jak i zamierzone zachowanie użytkowników, ale jest również zbyt długie do wdrożenia. Jeśli nie chcemy mieć „wygodnej” metody zwracającej obecny typ, o wiele łatwiej jest ją mieć public virtual object Clone()
.
Zobaczmy więc „ostateczne” rozwiązanie - co w C # ma naprawdę na celu dać nam komfort?
public class Base : ICloneable
{
public virtual object Clone()
{
return new Base();
}
}
public class Derived : Base
{
public override object Clone()
{
return new Derived();
}
}
public static T Copy<T>(this T obj) where T : class, ICloneable
{
return obj.Clone() as T;
}
Nazywa się Kopiuj, aby nie kolidować z bieżącymi metodami klonowania (kompilator preferuje metody zadeklarowane przez typ niż metody rozszerzenia). Obowiązuje class
ograniczenie prędkości (nie wymaga sprawdzania wartości zerowej itp.).
Mam nadzieję, że to wyjaśnia powód, dlaczego tego nie robić ICloneable<T>
. Jednak zaleca się, aby ICloneable
w ogóle nie wdrażać .
ICloneable
jest dla typów wartości, w których może to obejść boksowanie metody klonowania i sugeruje, że masz wartość rozpakowaną. A ponieważ struktury mogą być klonowane (płytko) automatycznie, nie ma potrzeby ich implementacji (chyba że określisz, że oznacza to głęboką kopię).
Chociaż pytanie jest bardzo stare (5 lat od napisania tych odpowiedzi :) i zostało już udzielone, ale znalazłem ten artykuł dość dobrze odpowiada na pytanie, sprawdź tutaj
EDYTOWAĆ:
Oto cytat z artykułu, który odpowiada na pytanie (koniecznie przeczytaj cały artykuł, zawiera on inne interesujące rzeczy):
W Internecie istnieje wiele odniesień wskazujących na posta na blogu z 2003 roku autorstwa Brada Abramsa - wówczas zatrudnionego w firmie Microsoft - w którym omawiane są niektóre przemyślenia na temat ICloneable. Wpis na blogu można znaleźć pod tym adresem: Implementing ICloneable . Pomimo mylącego tytułu ten wpis na blogu wzywa do niewprowadzania ICloneable, głównie z powodu płytkiego / głębokiego zamieszania. Artykuł kończy się prostą sugestią: jeśli potrzebujesz mechanizmu klonowania, zdefiniuj własną metodologię klonowania lub kopiowania i upewnij się, że jasno dokumentujesz, czy jest to głęboka czy płytka kopia. Odpowiednim wzorem jest:
public <type> Copy();
Dużym problemem jest to, że nie mogliby ograniczyć T do tej samej klasy. Na przykład, co uniemożliwiłoby Ci to:
interface IClonable<T>
{
T Clone();
}
class Dog : IClonable<JackRabbit>
{
//not what you would expect, but possible
JackRabbit Clone()
{
return new JackRabbit();
}
}
Potrzebują ograniczenia parametrów, takiego jak:
interfact IClonable<T> where T : implementing_type
class A : ICloneable { public object Clone() { return 1; } /* I can return whatever I want */ }
ICloneable<T>
mógł ograniczyć się T
do dopasowania do własnego typu, nie wymusiłoby to implementacji Clone()
zwracania czegokolwiek zdalnie przypominającego obiekt, na którym został sklonowany. Ponadto, chciałbym zaproponować, że jeśli ktoś jest za pomocą interfejsu kowariancji, może być najlepiej mieć klasy, które implementują ICloneable
być uszczelnione, że interfejs ICloneable<out T>
obejmują Self
nieruchomości, które oczekuje się, aby powrócić sobie i ...
ICloneable<BaseType>
lub ICloneable<ICloneable<BaseType>>
. BaseType
W pytaniu powinny mieć protected
metodę klonowania, który będzie nazywany przez typ, który implementuje ICloneable
. Ten projekt dopuszcza możliwość, że ktoś może chcieć mieć a Container
, a CloneableContainer
, a FancyContainer
, a CloneableFancyContainer
ten drugi jest użyteczny w kodzie, który wymaga klonowalnej pochodnej Container
lub który wymaga FancyContainer
(ale nie obchodzi go, czy można go klonować).
FancyList
typ, który można rozsądnie sklonować, ale pochodna może automatycznie utrzymywać swój stan w pliku dyskowym (określonym w konstruktorze). Nie można sklonować typu pochodnego, ponieważ jego stan byłby dołączony do stanu zmiennego singletona (pliku), ale nie powinno to wykluczać użycia typu pochodnego w miejscach, które wymagają większości funkcji, FancyList
ale nie potrzebują sklonować to.
To bardzo dobre pytanie ... Możesz jednak stworzyć własne:
interface ICloneable<T> : ICloneable
{
new T Clone ( );
}
Andrey twierdzi, że jest to zły interfejs API, ale nie słyszałem o tym, żeby ten interfejs był przestarzały. I to zniszczyłoby mnóstwo interfejsów ... Metoda klonowania powinna wykonać płytką kopię. Jeśli obiekt zapewnia również głęboką kopię, można użyć przeciążonego klonu (bool deep).
EDYCJA: Wzór, którego używam do „klonowania” obiektu, przekazuje prototyp do konstruktora.
class C
{
public C ( C prototype )
{
...
}
}
Usuwa to wszelkie potencjalne nadmiarowe sytuacje związane z implementacją kodu. BTW, mówiąc o ograniczeniach ICloneable, czy tak naprawdę nie do samego obiektu należy decyzja, czy należy wykonać płytki klon, czy głęboki klon, czy nawet częściowo płytki / częściowo głęboki klon? Czy naprawdę powinno nas to obchodzić, o ile obiekt działa zgodnie z przeznaczeniem? W niektórych przypadkach dobre wdrożenie klonowania może równie dobrze obejmować zarówno płytkie, jak i głębokie klonowanie.