Jakie jest właściwe zastosowanie downcastingu?


67

Downcasting oznacza rzutowanie z klasy podstawowej (lub interfejsu) do podklasy lub klasy liścia.

Przykładem downcast może być rzutowanie z System.Objectinnego typu.

Downcasting jest niepopularny, może pachnie kodem: doktryna obiektowa preferuje na przykład definiowanie i wywoływanie metod wirtualnych lub abstrakcyjnych zamiast downcastingu.

  • Jakie, jeśli w ogóle, są dobre i właściwe przypadki użycia dla downcastingu? To znaczy, w jakich okolicznościach należy pisać kod, który jest w dół?
  • Jeśli twoja odpowiedź brzmi „brak”, to dlaczego język obsługuje downcasting?

5
To, co opisujesz, nazywa się zwykle „downcastingiem”. Uzbrojony w tę wiedzę, czy mógłbyś najpierw przeprowadzić więcej własnych badań i wyjaśnić, dlaczego istniejące powody, które znalazłeś, nie są wystarczające? TL; DR: downcasting przerywa pisanie statyczne, ale czasami system typów jest zbyt restrykcyjny. Dlatego rozsądne jest, aby język oferował bezpieczne downcasting.
amon

Masz na myśli downcasting? Upcasting byłby wyższy w hierarchii klas, tj. Od podklasy do nadklasy, w przeciwieństwie do downcastingu, od nadklasy do podklasy ...
Andrei Socaciu

Przepraszam: masz rację. Zredagowałem pytanie, aby zmienić nazwę „upcast” na „downcast”.
ChrisW

@amon en.wikipedia.org/wiki/Downcasting podaje jako przykład „konwersję na ciąg” (który .Net obsługuje przy użyciu wirtualnego ToString); jego drugim przykładem są „kontenery Java”, ponieważ Java nie obsługuje generics (co robi C #). stackoverflow.com/questions/1524197/downcast-and-upcast mówi, co to jest downcasting , ale bez przykładów, kiedy jest to właściwe .
ChrisW

4
@ChrisW To before Java had support for genericsnie jest because Java doesn't support generics. I amon prawie dał ci odpowiedź - kiedy system typów, z którym pracujesz, jest zbyt restrykcyjny i nie jesteś w stanie go zmienić.
Ordous

Odpowiedzi:


12

Oto niektóre właściwe zastosowania downcastingu.

I szacunkiem stanowczo nie zgadzają się z innymi, którzy mówią tu zastosowanie downcasts jest zdecydowanie zapach kod, ponieważ wierzę, nie ma innego rozsądny sposób na rozwiązanie odpowiednie problemy.

Equals:

class A
{
    // Nothing here, but if you're a C++ programmer who dislikes object.Equals(object),
    // pretend it's not there and we have abstract bool A.Equals(A) instead.
}
class B : A
{
    private int x;
    public override bool Equals(object other)
    {
        var casted = other as B;  // cautious downcast (dynamic_cast in C++)
        return casted != null && this.x == casted.x;
    }
}

Clone:

class A : ICloneable
{
    // Again, if you dislike ICloneable, that's not the point.
    // Just ignore that and pretend this method returns type A instead of object.
    public virtual object Clone()
    {
        return this.MemberwiseClone();  // some sane default behavior, whatever
    }
}
class B : A
{
    private int[] x;
    public override object Clone()
    {
        var copy = (B)base.Clone();  // known downcast (static_cast in C++)
        copy.x = (int[])this.x.Clone();  // oh hey, another downcast!!
        return copy;
    }
}

Stream.EndRead/Write:

class MyStream : Stream
{
    private class AsyncResult : IAsyncResult
    {
        // ...
    }
    public override int EndRead(IAsyncResult other)
    {
        return Blah((AsyncResult)other);  // another downcast (likely ~static_cast)
    }
}

Jeśli chcesz powiedzieć, że są to zapachy kodowe, musisz zapewnić im lepsze rozwiązania (nawet jeśli można ich uniknąć z powodu niedogodności). Nie wierzę, że istnieją lepsze rozwiązania.


9
„Zapach kodowy” nie oznacza „ten projekt jest zdecydowanie zły ”, ale oznacza „ten projekt jest podejrzany ”. To, że istnieją cechy językowe, które są z natury podejrzane, jest kwestią pragmatyzmu ze strony autora języka
Caleth

5
@Caleth: I moim celem jest to, że te projekty pojawiają się cały czas i że absolutnie nic nie jest podejrzane w projektach, które pokazałem, a zatem casting nie jest zapachem kodu.
Mehrdad

4
@Caleth Nie zgadzam się. Zapach kodu oznacza dla mnie, że kod jest zły lub przynajmniej można go poprawić.
Konrad Rudolph

6
@Mehrdad Twoja opinia wydaje się być taka, że ​​zapachy kodu muszą być winą programistów aplikacji. Dlaczego? Nie każdy zapach kodu musi stanowić rzeczywisty problem lub mieć rozwiązanie. Zapach kodu według Martina Fowlera to po prostu „wskazanie powierzchni, które zwykle odpowiada głębszemu problemowi w systemie”, object.Equalspasuje do tego opisu całkiem dobrze (postaraj się poprawnie dostarczyć kontrakt równy w nadklasie, który pozwala na prawidłowe wdrożenie podklas - to absolutnie nietrywialne). To, że jako programiści aplikacji nie możemy tego rozwiązać (w niektórych przypadkach możemy tego uniknąć) to inny temat.
Voo

5
@ Mehrdad Zobaczmy, co jeden z twórców oryginalnego języka ma do powiedzenia na temat Equals .. Object.Equals is horrid. Equality computation in C# is completely messed up(komentarz Erica do jego odpowiedzi). To zabawne, że nie chcesz ponownie rozważać swojej opinii, nawet jeśli jeden z głównych projektantów samego języka mówi ci, że się mylisz.
Voo

135

Downcasting jest niepopularny, może zapach kodu

Nie zgadzam się. Downcasting jest niezwykle popularny ; ogromna liczba programów w świecie rzeczywistym zawiera co najmniej jeden problem. I to może nie jest zapach kodu. To zdecydowanie zapach kodu. Dlatego operacja downcastingu musi być zamanifestowana w tekście programu . Dzięki temu łatwiej jest zauważyć zapach i poświęcić mu uwagę na przeglądanie kodu.

w jakich okolicznościach należy napisać kod, który nie działa?

W każdych okolicznościach, gdy:

  • masz w 100% poprawną wiedzę o fakcie dotyczącym typu środowiska wykonawczego wyrażenia, które jest bardziej szczegółowe niż typ wyrażenia w czasie kompilacji, oraz
  • musisz skorzystać z tego faktu, aby skorzystać z możliwości obiektu niedostępnego w typie kompilacji oraz
  • lepsze wykorzystanie czasu i wysiłku na napisanie obsady, niż refaktoryzacja programu w celu wyeliminowania jednego z dwóch pierwszych punktów.

Jeśli możesz tanio refaktoryzować program, tak aby kompilator mógł wydedukować typ środowiska wykonawczego, lub refaktoryzować program, aby nie trzeba było możliwości bardziej pochodnego typu, zrób to. Do języka dodano downcasty w tych okolicznościach, w których refaktoryzacja programu jest trudna i droga .

dlaczego język obsługuje downcasting?

C # został wymyślony przez pragmatycznych programistów, którzy mają zadania do wykonania, dla pragmatycznych programistów, którzy mają zadania do wykonania. Projektanci C # nie są purystami OO. A system typu C # nie jest idealny; z założenia nie docenia ograniczeń, które można nałożyć na typ środowiska wykonawczego zmiennej.

Również downcasting jest bardzo bezpieczny w C #. Mamy silną gwarancję, że downcast zostanie zweryfikowany w czasie wykonywania, a jeśli nie będzie można go zweryfikować, program wykona właściwą czynność i ulegnie awarii. To jest cudowne; oznacza to, że jeśli twoje 100% poprawne zrozumienie semantyki typów okaże się 99,99% poprawne, to twój program działa przez 99,99% czasu i zawiesza resztę czasu, zamiast zachowywać się nieprzewidywalnie i uszkadzać dane użytkownika 0,01% czasu .


ĆWICZENIE: Istnieje co najmniej jeden sposób na utworzenie downcastu w języku C # bez użycia jawnego operatora rzutowania. Czy możesz wymyślić takie scenariusze? Ponieważ są to również potencjalne zapachy kodu, jakie według Pana czynniki projektowe przyczyniły się do zaprojektowania funkcji, która może spowodować awarię w dół, bez manifestu w kodzie?


101
Pamiętasz te plakaty z liceum na ścianach „Co jest popularne, nie zawsze jest właściwe, co jest dobre, nie zawsze jest popularne?” Myślałeś, że próbują powstrzymać cię od brania narkotyków lub zajścia w ciążę w szkole średniej, ale w rzeczywistości były ostrzeżeniami przed niebezpieczeństwem zadłużenia technicznego.
corsiKa

14
@EricLippert, oczywiście oświadczenie foreach. Stało się tak, zanim IEnumerable był ogólny, prawda?
Arturo Torres Sánchez

18
To wyjaśnienie przyniosło mi dzień. Jako nauczyciel często jestem pytany, dlaczego C # jest zaprojektowany w taki czy inny sposób, a moje odpowiedzi często oparte są na motywacjach, które Ty i Twoi koledzy podzieliliście tutaj i na waszych blogach. Często trudniej jest odpowiedzieć na pytanie uzupełniające „Ale dlaczego tak ?!”. I tutaj znajduję idealną odpowiedź następczą: „C # został wymyślony przez pragmatycznych programistów, którzy mają zadania do wykonania, dla pragmatycznych programistów, którzy mają zadania do wykonania”. :-) Dziękuję @EricLippert!
Zano

12
Na marginesie: oczywiście w moim powyższym komentarzu chciałem powiedzieć, że mamy spuściznę z punktu A do B. Naprawdę nie lubię używania słów „góra” i „dół” oraz „rodzic” i „dziecko” podczas nawigowania w hierarchii klas i dostaję je źle cały czas piszę. Zależność między typami jest relacją nadzbiór / podzbiór, więc dlaczego nie powinien być z tym związany kierunek w górę lub w dół, nie wiem. I nawet nie zaczynaj mnie od „rodzica”. Rodzicem Giraffe nie jest Mammal, rodzicem Giraffe jest Pan i Pani Giraffe.
Eric Lippert

9
Object.Equalsjest okropny . Obliczenia równości w języku C # są całkowicie pomieszane , zdecydowanie zbyt pomieszane, aby wyjaśnić w komentarzu. Jest tyle sposobów na reprezentowanie równości; bardzo łatwo jest uczynić je niespójnymi; jest to bardzo łatwe, w rzeczywistości wymaga naruszenia właściwości wymaganych od operatora równości (symetrii, zwrotności i przechodniości). Gdybym dzisiaj projektował system typów od zera, nie byłoby Equals( GetHashcodelub ToString) Objectw ogóle.
Eric Lippert

33

Procedury obsługi zdarzeń zwykle mają podpis MethodName(object sender, EventArgs e). W niektórych przypadkach można obsłużyć zdarzenie bez względu na typ sender, a nawet w ogóle go nie używać sender, ale w innych sendernależy obsłużyć bardziej konkretny typ, aby obsłużyć zdarzenie.

Na przykład możesz mieć dwa TextBoxs i chcesz użyć jednego delegata do obsługi zdarzenia z każdego z nich. Następnie musisz rzutować senderna TextBox, aby uzyskać dostęp do niezbędnych właściwości do obsługi zdarzenia:

private void TextBox_TextChanged(object sender, EventArgs e)
{
   TextBox box = (TextBox)sender;
   box.BackColor = string.IsNullOrEmpty(box.Text) ? Color.Red : Color.White;
}

Tak, w tym przykładzie możesz utworzyć własną podklasę, TextBoxktóra zrobiła to wewnętrznie. Jednak nie zawsze jest to praktyczne lub możliwe.


2
Dziękuję, to dobry przykład. TextChangedWydarzenie jest członkiem Control- Zastanawiam się, dlaczego sendernie jest typu Controlzamiast typu object? Zastanawiam się także, czy (teoretycznie) środowisko mogło przedefiniować zdarzenie dla każdej podklasy (tak, że np. TextBoxMoże mieć nową wersję zdarzenia, używając TextBoxjako typu nadawcy) ... które może (lub nie) wymagać downcastingu ( do TextBox) wewnątrz TextBoximplementacji (w celu zaimplementowania nowego typu zdarzenia), ale uniknęłby konieczności downcastu w module obsługi zdarzeń kodu aplikacji.
ChrisW

3
Jest to uważane za dopuszczalne, ponieważ trudno jest uogólnić obsługę zdarzeń, jeśli każde zdarzenie ma własną sygnaturę metody. Chociaż przypuszczam, że jest to przyjęte ograniczenie, a nie coś, co uważa się za najlepszą praktykę, ponieważ alternatywa jest w rzeczywistości bardziej kłopotliwa. Gdyby można było zacząć od TextBox senderzamiast object senderbez nadmiernego komplikowania kodu, jestem pewien, że tak by było.
Neil

6
@Neil Systemy zdarzeń są specjalnie zaprojektowane, aby umożliwić przekazywanie dowolnych porcji danych do dowolnych obiektów. To prawie niemożliwe, aby obejść się bez sprawdzania poprawności typu w czasie wykonywania.
Joker_vD

@Joker_vD Idealnie byłoby, gdyby interfejs API obsługiwał silnie typowane sygnatury wywołania zwrotnego przy użyciu sprzeczności ogólnej. Fakt, że .NET (w przeważającej części) tego nie robi, wynika jedynie z faktu, że generyczne i kowariancje zostały wprowadzone później. - Innymi słowy, downcasting tutaj (i ogólnie gdzie indziej) jest wymagany z powodu niedoskonałości języka / API, a nie dlatego, że jest to zasadniczo dobry pomysł.
Konrad Rudolph

@KonradRudolph Pewnie, ale jak przechowywać zdarzenia dowolnego typu w kolejce zdarzeń? Czy po prostu pozbywasz się kolejki i idziesz do bezpośredniej wysyłki?
Joker_vD

17

Potrzebujesz downcastingu, gdy coś daje ci nadtyp i musisz postępować inaczej w zależności od podtypu.

decimal value;
Donation d = user.getDonation();
if (d is CashDonation) {
     value = ((CashDonation)d).getValue();
}
if (d is ItemDonation) {
     value = itemValueEstimator.estimateValueOf(((ItemDonation)d).getItem());
}
System.out.println("Thank you for your generous donation of $" + value);

Nie, to nie pachnie dobrze. W idealnym świecie Donationistniałaby abstrakcyjna metoda, getValuea każdy podtyp implementujący miałby odpowiednią implementację.

Ale co, jeśli te klasy pochodzą z biblioteki, której nie możesz lub nie chcesz zmieniać? Wtedy nie ma innej opcji.


To wygląda na dobry moment na użycie wzoru odwiedzającego. Lub dynamicsłowo kluczowe, które osiąga ten sam wynik.
user2023861

3
@ user2023861 Nie Myślę, że wzorzec odwiedzających zależy od współpracy z podklasami darowizny - tzn. darowizna musi zadeklarować streszczenie void accept(IDonationVisitor visitor), które jego podklasy implementują, wywołując określone (np. przeciążone) metody IDonationVisitor.
ChrisW

@ChrisW, masz rację. Bez współpracy nadal można to zrobić za pomocą dynamicsłowa kluczowego.
user2023861

W tym konkretnym przypadku możesz dodać metodę szacunkową do Darowizny.
immibis

@ user2023861: dynamicwciąż spuszczanie, tylko wolniej.
Mehrdad

12

Aby dodać do odpowiedzi Erica Lipperta, ponieważ nie mogę komentować ...

Często można uniknąć downcastingu, refaktoryzując interfejs API w celu użycia ogólnych. Ale generyczne nie zostały dodane do języka aż do wersji 2.0 . Więc nawet jeśli twój własny kod nie musi obsługiwać starożytnych wersji językowych, możesz użyć starszych klas, które nie są sparametryzowane. Na przykład, można zdefiniować pewną klasę do przenoszenia ładunku typu Object, który musisz rzucić na typ, o którym wiesz, że tak naprawdę jest.


Rzeczywiście, można powiedzieć, że generics w Javie lub C # są w zasadzie tylko bezpiecznym opakowaniem do przechowywania obiektów nadklasy i sprowadzania do właściwej podklasy (sprawdzonej przez kompilator) przed ponownym użyciem elementów. (W przeciwieństwie do szablonów C ++, które przepisują kod, aby zawsze korzystać z samej podklasy, a tym samym unikać konieczności obniżania - co może zwiększyć wydajność pamięci, choć często kosztem czasu kompilacji i rozmiaru pliku wykonywalnego.)
lewo około

4

Istnieje kompromis między językami statycznymi i dynamicznie typowanymi. Wpisywanie statyczne daje kompilatorowi wiele informacji, dzięki którym można uzyskać dość silne gwarancje bezpieczeństwa (części) programów. Jest to jednak kosztowne, ponieważ nie tylko musisz wiedzieć, że twój program jest poprawny, ale musisz napisać odpowiedni kod, aby przekonać kompilator, że tak jest. Innymi słowy, zgłaszanie roszczeń jest łatwiejsze niż ich udowodnienie.

Istnieją „niebezpieczne” konstrukcje, które pomagają w tworzeniu niepotwierdzonych twierdzeń dotyczących kompilatora. Na przykład bezwarunkowe wezwania do Nullable.Value, bezwarunkowe sprowadzenia, dynamicobiekty itp. Pozwalają one na dochodzenie roszczenia („Twierdzę, że obiekt ajest String, jeśli się mylę, rzucają InvalidCastException”) bez konieczności jego udowodnienia. Może to być przydatne w przypadkach, gdy udowodnienie, że jest to znacznie trudniejsze niż opłacalne.

Nadużywanie tego jest ryzykowne i właśnie dlatego istnieje wyraźna notacja w dół i jest obowiązkowa; to sól syntaktyczna, która ma zwrócić uwagę na niebezpieczną operację. Język mógł zostać zaimplementowany z niejawnym downcastingiem (gdzie wywnioskowany typ jest jednoznaczny), ale to ukryłoby tę niebezpieczną operację, co nie jest pożądane.


Wydaje mi się, że pytam więc o kilka przykładów, gdzie / kiedy jest to konieczne lub pożądane (tj. Dobra praktyka), aby dokonać tego rodzaju „niepotwierdzonego stwierdzenia”.
ChrisW

1
Co powiesz na rzutowanie wyniku operacji odbicia? stackoverflow.com/a/3255716/3141234
Alexander

3

Downcasting jest uważany za zły z kilku powodów. Przede wszystkim myślę, ponieważ jest to anty-OOP.

OOP naprawdę by się to spodobało, gdybyś nigdy nie musiał być przygnębiony, ponieważ jego „raison-d'etre” polega na tym, że polimorfizm oznacza, że ​​nie musisz iść

if(object is X)
{
    //do the thing we do with X's
}
else
{
    //of the thing we do with Y's
}

po prostu to robisz

x.DoThing()

a kod auto-magicznie robi właściwą rzecz.

Istnieje kilka „trudnych” powodów, aby nie spuszczać:

  • W C ++ jest wolny.
  • Jeśli wybierzesz niewłaściwy typ, pojawi się błąd czasu wykonywania.

Ale alternatywa dla downcastingu w niektórych scenariuszach może być dość brzydka.

Klasycznym przykładem jest przetwarzanie komunikatów, w którym nie chcesz dodawać funkcji przetwarzania do obiektu, ale przechowywać go w procesorze komunikatów. Następnie mam MessageBaseClassdo przetworzenia tonę w tablicy, ale do poprawnego przetwarzania potrzebuję każdej z nich według podtypu.

Możesz użyć Double Dispatch, aby obejść problem ( https://en.wikipedia.org/wiki/Double_dispatch ) ... Ale to też ma swoje problemy. Lub możesz po prostu sprowadzić prosty kod na ryzyko tych trudnych powodów.

Ale to było zanim wynaleziono Generics. Teraz możesz uniknąć downcastingu, podając typ, w którym szczegóły zostaną określone później.

MessageProccesor<T> może zmieniać parametry metody i zwracane wartości o określony typ, ale nadal zapewnia ogólną funkcjonalność

Oczywiście nikt nie zmusza Cię do pisania kodu OOP, a jest wiele funkcji językowych, które są dostępne, ale są niezadowolone, na przykład refleksja.


1
Wątpię, żeby w C ++ działał wolno, zwłaszcza jeśli static_castzamiast niego dynamic_cast.
ChrisW

„Przetwarzanie wiadomości” - myślę, że oznacza to np. Rzutowanie lParamna coś konkretnego. Nie jestem jednak pewien, czy to dobry przykład w języku C #.
ChrisW

3
@ChrisW - cóż, tak, static_castzawsze jest szybki ... ale nie ma gwarancji, że nie zawiedzie w nieokreślony sposób, jeśli popełnisz błąd, a typ nie jest tym, czego oczekujesz.
Jules

@Jules: To zupełnie nie ma sensu.
Mehrdad

@ Mehrdad, właśnie o to chodzi. zrobienie równoważnego c # cast w c ++ jest powolne. i to jest tradycyjny powód do rzucania. alternatywny static_cast nie jest równoważny i uważany za gorszy wybór z powodu nieokreślonego zachowania
Ewan

0

Oprócz wszystkich wcześniejszych, wyobraź sobie właściwość Tag, która jest typu obiektowego. Umożliwia przechowywanie wybranego obiektu w innym obiekcie do późniejszego wykorzystania w razie potrzeby. Musisz sprowadzić tę właściwość.

Ogólnie rzecz biorąc, nieużywanie czegoś do dzisiaj nie zawsze oznacza coś bezużytecznego ;-)


Tak, patrz na przykład: Do czego służy właściwość Tag w .net . W jednej z odpowiedzi, ktoś napisał, użyłem go do wprowadzenia instrukcji do użytkownika w aplikacjach Windows Forms. Kiedy zdarzenie sterujące GotFocus zostało uruchomione, do właściwości Label.Text przypisano wartość mojej właściwości sterującej Tag, która zawierała ciąg instrukcji. Wydaje mi się, że alternatywnym sposobem na „rozszerzenie” Control(zamiast używania object Tagwłaściwości) byłoby utworzenie Dictionary<Control, string>etykiety do przechowywania każdej kontrolki.
ChrisW

1
Podoba mi się ta odpowiedź, ponieważ Tagjest to własność publiczna klasy frameworku .Net, która pokazuje, że (mniej więcej) powinieneś z niej korzystać.
ChrisW

@ChrisW: Nie mówię, że wniosek jest błędny (lub słuszny), ale zauważ, że to niechlujne rozumowanie, ponieważ istnieje wiele publicznych funkcji .NET, które przestarzałe (powiedzmy System.Collections.ArrayList) lub w inny sposób przestarzałe (powiedzmy IEnumerator.Reset).
Mehrdad

0

Najczęstszym zastosowaniem downcastingu w mojej pracy jest to, że jakiś idiota łamie zasadę substytucji Liskowa.

Wyobraź sobie, że w niektórych bibliotekach zewnętrznych jest interfejs.

public interface Foo
{
     void SayHello();
     void SayGoodbye();
 }

I 2 klasy, które to implementują. Bara Qux. Barjest dobrze wychowany.

public class Bar
{
    void SayHello()
    {
        Console.WriteLine(“Bar says hello.”);
    }
    void SayGoodbye()
    {
         Console.WriteLine(“Bar says goodbye”);
    }
}

Ale Quxnie jest dobrze wychowany.

public class Qux
{
    void SayHello()
    {
        Console.WriteLine(“Qux says hello.”);
    }
    void SayGoodbye()
    {
        throw new NotImplementedException();
    }
}

Cóż ... teraz nie mam wyboru. I mają na typ kontroli i (ewentualnie) przygnębiony, aby uniknąć mój program zatrzymał się upaść.


1
Nieprawda w tym konkretnym przykładzie. Możesz po prostu złapać NotImplementedException podczas wywoływania SayGoodbye.
Per von Zweigbergk

To może być zbyt uproszczony przykład, ale trochę pracuj z niektórymi starszymi interfejsami API .Net i napotkasz taką sytuację. Czasami najprostszym sposobem na napisanie kodu jest bezpieczne przesłanie ala var qux = foo as Qux;.
RubberDuck

0

Innym przykładem w świecie rzeczywistym jest WPF VisualTreeHelper. Wykorzystuje downcast do przesyłania DependencyObjectdo Visuali Visual3D. Na przykład VisualTreeHelper.GetParent . Downcast odbywa się w VisualTreeUtils.AsVisualHelper :

private static bool AsVisualHelper(DependencyObject element, out Visual visual, out Visual3D visual3D)
{
    Visual elementAsVisual = element as Visual;

    if (elementAsVisual != null)
    {
        visual = elementAsVisual;
        visual3D = null;
        return true;
    }

    Visual3D elementAsVisual3D = element as Visual3D;

    if (elementAsVisual3D != null)
    {
        visual = null;
        visual3D = elementAsVisual3D;
        return true;
    }            

    visual = null;
    visual3D = null;
    return false;
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.