Kiedy testowanie typu jest OK?

53

Zakładając język z pewnym nieodłącznym rodzajem bezpieczeństwa (np. Nie JavaScript):

Biorąc pod uwagę metodę, która akceptuje a SuperType, wiemy, że w większości przypadków, w których możemy ulec pokusie przeprowadzenia testów typu w celu wybrania akcji:

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    o.doSomethingA()
  } else {
    o.doSomethingB();
  }
}

Zwykle powinniśmy, jeśli nie zawsze, stworzyć jedną, nadającą się do zastąpienia metodę SuperTypei zrobić to:

public void DoSomethingTo(SuperType o) {
  o.doSomething();
}

... w którym każdy podtyp otrzymuje własną doSomething()implementację. Reszta naszej aplikacji może być odpowiednio nieświadoma, czy dana dana SuperTypejest naprawdę SubTypeAa SubTypeB.

Wspaniale.

Ale wciąż mamy is apodobne operacje w większości, jeśli nie we wszystkich, językach bezpiecznych dla typu. A to sugeruje potencjalną potrzebę jawnego testowania typu.

Tak więc, w jakich sytuacjach, jeśli w ogóle, powinny nas lub musi wykonujemy wyraźny badanie typu?

Wybacz mi moją nieobecność lub brak kreatywności. Wiem, że już to zrobiłem; ale to było tak dawno temu, że nie pamiętam, czy to, co zrobiłem, było dobre! W niedawnej pamięci nie sądzę, że spotkałem się z potrzebą testowania typów poza moim kowbojskim JavaScript.

svidgen
źródło
1
Przeczytaj o wnioskowaniu typu i systemach typów
Basile Starynkevitch,
4
Warto zauważyć, że ani Java, ani C # nie miały w swojej pierwszej wersji generyków. Trzeba było rzucać do iz Object, aby używać kontenerów.
Doval
6
„Sprawdzanie typów” prawie zawsze odnosi się do sprawdzania, czy kod przestrzega statycznych reguł pisania w języku. To, co masz na myśli, nazywa się zwykle testowaniem typu .
3
Dlatego lubię pisać C ++ i pozostawić wyłączony RTTI. Gdy dosłownie nie można przetestować typów obiektów w czasie wykonywania, zmusza to programistę do przestrzegania dobrego projektu OO w odniesieniu do zadawanego tutaj pytania.

Odpowiedzi:

48

„Nigdy” to kanoniczna odpowiedź na „kiedy testowanie typu jest w porządku?” Nie ma sposobu, aby to udowodnić lub obalić; jest częścią systemu przekonań o tym, co czyni „dobry projekt” lub „dobry projekt obiektowy”. To także hokum.

Oczywiście, jeśli masz zintegrowany zestaw klas, a także więcej niż jedną lub dwie funkcje, które wymagają tego rodzaju bezpośredniego testowania typu, prawdopodobnie robisz to źle. To, czego naprawdę potrzebujesz, to metoda, która jest różnie zaimplementowana SuperTypei jej podtypy. Jest to nieodłączna część programowania obiektowego i istnieje cały powód istnienia klas i dziedziczenia.

W tym przypadku jawne testowanie typu jest błędne nie dlatego, że testowanie typu jest z natury błędne, ale dlatego, że język ma już czysty, rozszerzalny, idiomatyczny sposób osiągnięcia dyskryminacji typów, a ty go nie użyłeś. Zamiast tego powróciliście do prymitywnego, kruchego, nierozszerzalnego idiomu.

Rozwiązanie: użyj idiomu. Jak zasugerowałeś, dodaj metodę do każdej z klas, a następnie pozwól, aby standardowe algorytmy dziedziczenia i wyboru metody określiły, który przypadek ma zastosowanie. Lub jeśli nie możesz zmienić typów podstawowych, podklas i dodaj tam swoją metodę.

Tyle o konwencjonalnej mądrości i kilku odpowiedziach. Niektóre przypadki, w których sensowne jest jawne testowanie typu:

  1. To jednorazowe. Jeśli miałeś dużo do czynienia z dyskryminacją typów, możesz rozszerzyć typy lub podklasę. Ale ty nie. Masz tylko jedno lub dwa miejsca, w których potrzebujesz jawnego testowania, więc nie warto wracać i pracować w hierarchii klas, aby dodać funkcje jako metody. Lub nie warto praktycznego wysiłku, aby dodać rodzaj ogólności, testowanie, recenzje projektu, dokumentację lub inne atrybuty klas podstawowych dla tak prostego, ograniczonego użycia. W takim przypadku dodanie funkcji wykonującej bezpośrednie testy jest racjonalne.

  2. Nie możesz dostosowywać klas. Myślisz o podklasach - ale nie możesz. Na przykład wyznaczono wiele klas w Javie final. Próbujesz wrzucić „a”, public class ExtendedSubTypeA extends SubTypeA {...} a kompilator mówi ci bez żadnych wątpliwości, że to, co robisz, nie jest możliwe. Przepraszamy, wdzięk i wyrafinowanie modelu obiektowego! Ktoś zdecydował, że nie możesz rozszerzyć ich typów! Niestety, wiele standardowych bibliotek jest final, a tworzenie klas finaljest powszechną wskazówką projektową. Końcowe uruchomienie funkcji jest dla ciebie dostępne.

    BTW, nie ogranicza się to do języków wpisywanych statycznie. Dynamiczny język Python ma wiele klas podstawowych, których pod osłonami zaimplementowanymi w C nie da się tak naprawdę modyfikować. Podobnie jak Java, obejmuje większość standardowych typów.

  3. Twój kod jest zewnętrzny. Tworzysz klasy i obiekty pochodzące z szeregu serwerów baz danych, silników oprogramowania pośredniego i innych baz kodu, których nie możesz kontrolować ani dostosowywać. Twój kod jest tylko niewielkim konsumentem obiektów generowanych gdzie indziej. Nawet jeśli możesz podklasę SuperType, nie będziesz w stanie uzyskać tych bibliotek, od których zależy generowanie obiektów w podklasach. Podadzą ci instancje typów, które znają, a nie twoje warianty. Nie zawsze tak jest ... czasami są budowane z myślą o elastyczności i dynamicznie tworzą instancje klas, które je karmisz. Lub zapewniają mechanizm rejestrowania podklas, które mają budować ich fabryki. Parsery XML wydają się szczególnie dobre w zapewnianiu takich punktów wejścia; patrz np lub lxml w Pythonie . Ale większość baz kodu nie zapewnia takich rozszerzeń. Oddają ci klasy, z którymi zostały zbudowane i o których wiedzą. Generalnie nie ma sensu umieszczać ich wyników w wynikach niestandardowych tylko po to, aby można było użyć selektora typu czysto obiektowego. Jeśli zamierzasz dokonać dyskryminacji typu, będziesz musiał to zrobić stosunkowo brutalnie. Twój kod testowania typu wygląda wtedy całkiem dobrze.

  4. Leki ogólne / wielokrotne dla biednych osób. Chcesz zaakceptować wiele różnych typów kodu i masz wrażenie, że posiadanie szeregu metod specyficznych dla typu nie jest taktowne. public void add(Object x)Wydaje się logiczne, ale nie tablicą addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, i addStringwariantów (by wymienić tylko kilka). Dzięki funkcjom lub metodom, które wymagają wysokiego typu, a następnie określają, co robić według rodzaju - nie zdobędą nagrody Czystości podczas corocznego sympozjum projektowego Booch-Liskov, ale rezygnują z Węgierskie nazewnictwo zapewni prostsze API. W pewnym sensie twój is-alubis-instance-of testowanie to symulacja generyczna lub wielozadaniowa w kontekście językowym, który natywnie tego nie obsługuje.

    Wbudowana obsługa języka zarówno dla ogólnych, jak i pisanych kaczych zmniejsza potrzebę sprawdzania typów, zwiększając prawdopodobieństwo „robienia czegoś wdzięcznego i odpowiedniego”. Wielokrotny wysyłka wybór / interfejs widziany w językach takich jak Julia i przejść podobnie zastąpić bezpośredniego badania typu z wbudowanymi mechanizmami selekcji opartej typu „co robić”. Ale nie wszystkie języki je obsługują. Jawa, na przykład, jest zasadniczo pojedyncza wysyłka, a jej idiomy nie są zbyt przyjazne do pisania na klawiaturze.

    Ale nawet przy wszystkich tych funkcjach dyskryminacji typów - dziedziczeniu, rodzajowych, pisaniu kaczych i wielokrotnym wysyłaniu - czasem wygodnie jest mieć jedną, skonsolidowaną procedurę, która sprawia, że ​​robisz coś w oparciu o typ obiektu jasne i natychmiastowe. W metaprogramowaniu stwierdziłem, że jest to w zasadzie nieuniknione. To, czy powrót do zapytań typu bezpośredniego stanowi „pragmatyzm w działaniu” czy „brudne kodowanie”, będzie zależeć od filozofii i przekonań projektowych.

Jonathan Eunice
źródło
Jeśli operacja nie może być wykonana na określonym typie, ale może być wykonana na dwóch różnych typach, do których byłaby domyślnie konwertowalna, przeciążenie jest odpowiednie tylko wtedy, gdy obie konwersje przyniosą identyczne wyniki. W przeciwnym razie (1.0).Equals(1.0f)uzyskujemy nieprzyzwoite zachowania, takie jak w .NET: zwracanie wartości true [argument promuje do double], ale (1.0f).Equals(1.0)dawanie wartości false [argument promuje do object]; w Javie Math.round(123456789*1.0)daje 123456789, ale Math.round(123456789*1.0)123456792 [argument promuje floatraczej niż double].
supercat
Jest to klasyczny argument przeciwko automatycznemu rzutowaniu / eskalacji typów. Złe i paradoksalne wyniki, przynajmniej w skrajnych przypadkach. Zgadzam się, ale nie jestem pewien, w jaki sposób zamierzasz odnosić się do mojej odpowiedzi.
Jonathan Eunice,
Odpowiadałem na twój punkt # 4, który wydawał się zalecać przeciążanie, zamiast używać metod o różnych nazwach z różnymi typami.
supercat
2
@supercat Obwiniaj mój słaby wzrok, ale te dwa wyrażenia Math.roundwyglądają identycznie jak ja. Co za różnica?
Lily Chung,
2
@IstvanChung: Ojej ... ta ostatnia powinna była Math.round(123456789)[wskazywać na to, co może się stać, jeśli ktoś przepisze, Math.round(thing.getPosition() * COUNTS_PER_MIL)aby zwrócić nieskalowaną wartość pozycji, nie zdając sobie sprawy, że getPositionzwraca intlub long.]
supercat
25

Główną sytuacją, jakiej kiedykolwiek potrzebowałem, była porównywanie dwóch obiektów, na przykład w equals(other)metodzie, która może wymagać różnych algorytmów w zależności od dokładnego typu other. Nawet wtedy jest to dość rzadkie.

Inną sytuacją, w której pojawiłem się, znowu bardzo rzadko, jest po deserializacji lub analizie składni, gdzie czasami potrzebujesz jej, aby bezpiecznie rzucić na bardziej konkretny typ.

Czasami potrzebujesz po prostu włamania do obejścia kodu, którego nie kontrolujesz. Jest to jedna z tych rzeczy, których tak naprawdę nie chcesz regularnie używać, ale cieszymy się, że jest tam, kiedy naprawdę jej potrzebujesz.

Karl Bielefeldt
źródło
1
Byłem przez chwilę zachwycony sprawą deserializacji, fałszywie pamiętając, że ją tam wykorzystałem; ale teraz nie wyobrażam sobie, jak bym to zrobił! Wiem, że przeprowadziłem w tym celu kilka dziwnych sprawdzeń typu; ale nie jestem pewien, czy to oznacza testowanie typu . Być może serializacja jest bliższą paralelą: przesłuchanie obiektów pod kątem ich konkretnego typu.
svidgen
1
Serializacja jest zwykle możliwa do wykonania przy użyciu polimorfizmu. Podczas deserializacji często robisz coś takiego BaseClass base = deserialize(input), ponieważ nie znasz jeszcze typu, a następnie if (base instanceof Derived) derived = (Derived)basezapisujesz go jako dokładny typ pochodny.
Karl Bielefeldt,
1
Równość ma sens. Z mojego doświadczenia wynika, że ​​takie metody mają często formę „jeśli te dwa obiekty mają ten sam konkretny typ, zwróć, czy wszystkie ich pola są równe; w przeciwnym razie zwróć false (lub nieporównywalny) ”.
Jon Purdy,
2
W szczególności fakt, że znajdziesz typ testu, jest dobrym wskaźnikiem, że porównania równości polimorficznej są gniazdem żmiji.
Steve Jessop,
12

Standardowy (ale miejmy nadzieję rzadki) przypadek wygląda następująco: w poniższej sytuacji

public void DoSomethingTo(SuperType o) {
  if (o isa SubTypeA) {
    DoSomethingA((SubTypeA) o )
  } else {
    DoSomethingB((SubTypeB) o );
  }
}

funkcje DoSomethingAlub DoSomethingBnie mogą być łatwo zaimplementowane jako funkcje składowe drzewa dziedziczenia SuperType/ SubTypeA/ SubTypeB. Na przykład jeśli

  • podtypy są częścią biblioteki, której nie można zmienić, lub
  • dodanie kodu DoSomethingXXXdo tej biblioteki oznaczałoby wprowadzenie zabronionej zależności.

Uwaga: często zdarzają się sytuacje, w których można obejść ten problem (na przykład, tworząc opakowanie lub adapter dla SubTypeAi SubTypeB, lub próbując DoSomethingcałkowicie zaimplementować w zakresie podstawowych operacji SuperType), ale czasami te rozwiązania nie są warte kłopotu lub sprawiają, że rzeczy bardziej skomplikowane i mniej rozszerzalne niż wykonywanie jawnego testu typu.

Przykład z mojej wczorajszej pracy: Miałem sytuację, w której zamierzałem zrównoleglić przetwarzanie listy obiektów (typu SuperType, z dokładnie dwoma różnymi podtypami, gdzie jest bardzo mało prawdopodobne, że będzie ich więcej). Niezrównana wersja zawierała dwie pętle: jedną pętlę dla obiektów podtypu A, wywołującą DoSomethingA, i drugą pętlę dla obiektów podtypu B, wywołującą DoSomethingB.

Metody „DoSomethingA” i „DoSomethingB” są zarówno czasochłonnymi obliczeniami, wykorzystującymi informacje kontekstowe, które nie są dostępne w zakresie podtypów A i B. (więc nie ma sensu implementować ich jako funkcji składowych podtypów). Z punktu widzenia nowej „równoległej pętli” znacznie ułatwia to, radząc sobie z nimi jednolicie, więc zaimplementowałem funkcję podobną do DoSomethingTopowyższej. Jednak spojrzenie na implementacje „DoSomethingA” i „DoSomethingB” pokazuje, że działają one bardzo odmiennie wewnętrznie. Więc próba wdrożenia ogólnego „DoSomething” poprzez rozszerzenie SuperTypeo wiele abstrakcyjnych metod tak naprawdę nie zadziałała lub oznaczałaby całkowite przeprojektowanie.

Doktor Brown
źródło
2
Czy jest szansa, że ​​możesz dodać mały, konkretny przykład, aby przekonać mój mglisty mózg, że ten scenariusz nie jest wymyślony?
svidgen
Mówiąc wprost, nie twierdzę, że jest wymyślony. Podejrzewam, że jestem dziś wyjątkowo szalony.
svidgen
@svidgen: jest to dalekie od bycia wymyślonym, w rzeczywistości dzisiaj spotkałem się z taką sytuacją (chociaż nie mogę opublikować tego przykładu tutaj, ponieważ zawiera wewnętrzne elementy biznesowe). Zgadzam się z innymi odpowiedziami, że użycie operatora „jest” powinno być wyjątkiem i powinno się to odbywać tylko w rzadkich przypadkach.
Doc Brown
Myślę, że twoja edycja, którą przeoczyłem, daje dobry przykład. ... Czy istnieją przypadki, w których to jest OK, nawet jeśli nie kontrolować SuperTypei to podklasy?
svidgen
6
Oto konkretny przykład: kontener najwyższego poziomu w odpowiedzi JSON z usługi sieci Web może być słownikiem lub tablicą. Zwykle masz jakieś narzędzie, które zamienia JSON w rzeczywiste obiekty (np. NSJSONSerializationW Obj-C), ale nie chcesz po prostu ufać, że odpowiedź zawiera oczekiwany typ, więc przed użyciem go sprawdź (np. if ([theResponse isKindOfClass:[NSArray class]])...) .
Caleb,
5

Jak nazywa to wujek Bob:

When your compiler forgets about the type.

W jednym ze swoich odcinków Clean Coder podał przykład wywołania funkcji używanej do zwracania Employees. Managerjest podtypem Employee. Załóżmy, że mamy usługę aplikacji, która akceptuje Manageridentyfikator i wzywa go do biura :) Funkcja getEmployeeById()zwraca supertyp Employee, ale chcę sprawdzić, czy w tym przypadku użycia został zwrócony menedżer.

Na przykład:

var manager = employeeRepository.getEmployeeById(empId);
if (!(manager is Manager))
   throw new Exception("Invalid Id specified.");
manager.summon();

Tutaj sprawdzam, czy pracownik zwrócony przez zapytanie jest w rzeczywistości menedżerem (tzn. Oczekuję, że będzie menedżerem, a jeśli nie, to szybko zawiedzie).

Nie najlepszy przykład, ale w końcu to wujek Bob.

Aktualizacja

Zaktualizowałem przykład tak bardzo, jak pamiętam z pamięci.

Songo
źródło
1
Dlaczego Managerimplementacja summon()nie rzuca wyjątku w tym przykładzie?
svidgen
@svidgen może CEOmoże wezwać Managers.
user253751,
@svidgen, Nie byłoby więc tak jasne, że pracownik Repository.getEmployeeById (empId) ma zwrócić menedżera
Ian
@ Ian Nie uważam tego za problem. Jeśli kod wywołujący jest prośbą o Employee, należy tylko dbać, że robi coś, co zachowuje się jak Employee. Jeśli różne podklasy Employeemają różne uprawnienia, obowiązki itp., Co sprawia, że ​​testowanie typu jest lepszą opcją niż prawdziwy system uprawnień?
svidgen
3

Kiedy sprawdzanie typu jest OK?

Nigdy.

  1. Mając powiązanie zachowania z tym typem w innej funkcji, naruszasz zasadę otwartej zamkniętej zasady , ponieważ możesz modyfikować istniejące zachowanie typu, zmieniając isklauzulę lub (w niektórych językach lub w zależności od scenariusz), ponieważ nie można rozszerzyć typu bez modyfikacji elementów wewnętrznych funkcji issprawdzającej.
  2. Co ważniejsze, isczeki są mocnym znakiem, że naruszasz Zasadę Zastępstwa Liskowa . Wszystko, co działa, SuperTypepowinno być całkowicie nieświadome, jakie mogą być podtypy.
  3. Niejawnie kojarzysz pewne zachowanie z nazwą typu. To sprawia, że ​​kod jest trudniejszy w utrzymaniu, ponieważ te niejawne umowy są rozłożone w całym kodzie i nie ma gwarancji, że będą uniwersalnie i konsekwentnie egzekwowane, tak jak w przypadku rzeczywistych członków klasy.

To powiedziawszy, isczeki mogą być mniej złe niż inne alternatywy. Umieszczenie wszystkich powszechnych funkcji w klasie bazowej jest ciężkie i często prowadzi do gorszych problemów. Używanie pojedynczej klasy, która ma flagę lub wyliczenie dla tego, jakiego „typu” instancja jest… jest gorsze niż okropne, ponieważ teraz rozprzestrzeniasz obchodzenie systemu typów na wszystkich konsumentów.

Krótko mówiąc, zawsze powinieneś uważać sprawdzanie typu za silny zapach kodu. Ale tak jak w przypadku wszystkich wytycznych, będą chwile, w których będziesz zmuszony wybrać, które naruszenie wytycznych jest najmniej obraźliwe.

Telastyn
źródło
3
Jest jedno drobne zastrzeżenie: implementacja algebraicznych typów danych w językach, które ich nie obsługują. Jednak korzystanie z dziedziczenia i sprawdzania typów jest tam jedynie nieuczciwym szczegółem implementacji; celem nie jest wprowadzenie podtypów, ale kategoryzowanie wartości. Mówię o tym tylko dlatego, że ADT są przydatne i nigdy nie są bardzo mocnym kwalifikatorem, ale poza tym w pełni się zgadzam; instanceofwycieka szczegóły implementacji i przerywa abstrakcję.
Doval
17
„Nigdy” to słowo, które tak naprawdę nie lubię w takim kontekście, zwłaszcza gdy jest sprzeczne z tym, co piszesz poniżej.
Doc Brown
10
Jeśli przez „nigdy” masz na myśli „czasami”, masz rację.
Caleb
2
OK oznacza dopuszczalne, dopuszczalne itp., Ale niekoniecznie idealne lub optymalne. Porównaj z nie OK : jeśli coś jest nie tak, nie powinieneś tego robić w ogóle. Jak wskazujesz, potrzeba sprawdzenia rodzaju czegoś może wskazywać na głębsze problemy w kodzie, ale są chwile, kiedy jest to najbardziej celowa, najmniej zła opcja, a w takich sytuacjach korzystanie z narzędzi na twoim komputerze jest oczywiście w porządku sprzedaż. (Gdyby łatwo było ich uniknąć we wszystkich sytuacjach, prawdopodobnie nie byłoby ich tam w pierwszej kolejności.) Pytanie sprowadza się do identyfikacji tych sytuacji i nigdy nie pomaga.
Caleb
2
@ superupat: Metody powinny wymagać możliwie najmniej określonego typu, który spełnia jego wymagania . IEnumerable<T>nie obiecuje, że istnieje „ostatni” element. Jeśli twoja metoda potrzebuje takiego elementu, powinna wymagać typu gwarantującego jego istnienie. Podtypy tego typu mogą następnie zapewnić wydajne implementacje metody „Last”.
cHao,
2

Jeśli masz dużą bazę kodu (ponad 100 000 wierszy kodu) i zbliżasz się do wysyłki lub pracujesz w oddziale, który później będzie musiał zostać scalony, a zatem zmiana sposobu radzenia sobie z nim wiąże się z dużym kosztem / ryzykiem.

Czasami masz wtedy opcję dużego refraktora systemu lub jakiegoś prostego zlokalizowanego „testowania typu”. Stwarza to dług techniczny, który powinien zostać spłacony jak najszybciej, ale często tak nie jest.

(Nie można podać przykładu, ponieważ każdy kod, który jest wystarczająco mały, aby go użyć jako przykład, jest również wystarczająco mały, aby lepszy projekt był wyraźnie widoczny.)

Innymi słowy, gdy celem jest wypłata wynagrodzenia, a nie „podniesienie głosów” za czystość projektu.


Innym częstym przypadkiem jest kod interfejsu użytkownika, gdy na przykład wyświetlasz inny interfejs użytkownika dla niektórych typów pracowników, ale oczywiście nie chcesz, aby koncepcje interfejsu użytkownika uciekały do ​​wszystkich klas „domeny”.

Możesz użyć „testowania typu”, aby zdecydować, która wersja interfejsu użytkownika ma zostać wyświetlona, ​​lub mieć jakąś fantazyjną tabelę odnośników, która konwertuje z „klas domen” do „klas interfejsu użytkownika”. Tabela przeglądowa jest tylko sposobem na ukrycie „testowania typu” w jednym miejscu.

(Kod aktualizujący bazę danych może mieć takie same problemy jak kod interfejsu użytkownika, jednak zwykle masz tylko jeden zestaw kodu aktualizującego bazę danych, ale możesz mieć wiele różnych ekranów, które muszą dostosować się do typu wyświetlanego obiektu.)

Ian
źródło
Wzorzec gościa jest często dobrym sposobem na rozwiązanie twojego problemu z interfejsem użytkownika.
Ian Goldby
@IanGoldby, zgadza się, że czasem tak może być, jednak nadal wykonujesz „testy typu”, tylko trochę ukryte.
Ian
Ukryty w tym samym sensie, co ukryty, gdy wywołujesz zwykłą metodę wirtualną? Czy masz na myśli coś innego? Wzorzec użytkownika, którego użyłem, nie ma instrukcji warunkowych, które zależą od typu. Wszystko odbywa się według języka.
Ian Goldby,
@IanGoldby, miałem na myśli ukryty w tym sensie, że może utrudnić zrozumienie kodu WPF lub WinForms. Oczekuję, że w przypadku niektórych podstawowych interfejsów WWW będzie działać bardzo dobrze.
Ian
2

Implementacja LINQ wykorzystuje wiele sprawdzania typu dla możliwych optymalizacji wydajności, a następnie powrót do IEnumerable.

Najbardziej oczywistym przykładem jest prawdopodobnie metoda ElementAt (niewielki fragment źródła .NET 4.5):

public static TSource ElementAt<TSource>(this IEnumerable<TSource> source, int index) { 
    IList<TSource> list = source as IList<TSource>;

    if (list != null) return list[index];
    // ... and then an enumerator is created and MoveNext is called index times

Ale w klasie Enumerable jest wiele miejsc, w których stosuje się podobny wzorzec.

Być może optymalizacja pod kątem wydajności dla często używanego podtypu jest poprawnym zastosowaniem. Nie jestem pewien, jak można to lepiej zaprojektować.

Menno van den Heuvel
źródło
Można by go lepiej zaprojektować, zapewniając wygodne środki, za pomocą których interfejsy mogą dostarczać domyślne implementacje, a następnie uwzględniając IEnumerable<T>wiele metod takich jak te List<T>, wraz z Featureswłaściwością wskazującą, które metody mogą działać dobrze, powoli lub wcale, a także różne założenia, które konsument może bezpiecznie poczynić na temat kolekcji (np. czy jej rozmiar i / lub istniejąca zawartość jest gwarantowana, że ​​nigdy się nie zmieni [typ może obsługiwać, Addjednocześnie gwarantując, że istniejąca zawartość będzie niezmienna]).
supercat
Z wyjątkiem scenariuszy, w których typ może wymagać trzymania jednej z dwóch zupełnie różnych rzeczy w różnych momentach, a wymagania są wyraźnie wykluczające się wzajemnie (a zatem użycie oddzielnych pól byłoby zbędne), ogólnie uważam potrzebę użycia rzutowania za znak członkowie, którzy powinni być częścią podstawowego interfejsu, nie byli. Nie oznacza to, że kod, który korzysta z interfejsu, który powinien obejmować niektórych członków, ale nie powinien używać rzutowania próbnego do obejścia pominięcia interfejsu podstawowego, ale te pisanie interfejsów bazowych powinno zminimalizować potrzebę próbowania rzutowania przez klientów.
supercat
1

Istnieje przykład, który często pojawia się w rozwoju gier, szczególnie w wykrywaniu kolizji, z którym trudno sobie poradzić bez użycia jakiejś formy testowania typu.

Załóżmy, że wszystkie obiekty gry pochodzą ze wspólnej klasy podstawowej GameObject. Każdy obiekt ma sztywny korpus kolizji kształt CollisionShape, który może zapewnić wspólny interfejs (co pozycji zapytania, orientacji itd), lecz rzeczywiste kształty kolizji wszystkie będą konkretne podklasy, takie jak Sphere, Box, ConvexHullitp przechowywanie informacji specyficznych dla danego typu geometryczny (zobacz tutaj prawdziwy przykład)

Teraz, aby przetestować kolizje, muszę napisać funkcję dla każdej pary typów kształtów kolizji:

detectCollision(Sphere, Sphere)
detectCollision(Sphere, Box)
detectCollision(Sphere, ConvexHull)
detectCollision(Box, ConvexHull)
...

które zawierają określoną matematykę potrzebną do wykonania przecięcia tych dwóch typów geometrycznych.

Przy każdym „tyknięciu” mojej pętli gry muszę sprawdzać, czy pary obiektów nie kolizją. Ale mam tylko dostęp do GameObjectsi odpowiadających im CollisionShape. Oczywiście muszę znać konkretne typy, aby wiedzieć, do której funkcji wykrywania kolizji należy zadzwonić. Nawet podwójna wysyłka (która logicznie nie różni się od sprawdzania typu) może tutaj pomóc *.

W praktyce w tej sytuacji silniki fizyki, które widziałem (Bullet i Havok), polegają na testowaniu typu takiej czy innej formy.

Nie twierdzę, że jest to dobre rozwiązanie, po prostu może być najlepszym z niewielkiej liczby możliwych rozwiązań tego problemu

* Technicznie jest to możliwe użycie podwójnego wysyłkę w straszliwej i skomplikowany sposób, który wymagałby n (n + 1) / 2 kombinacje (gdzie n oznacza liczbę typów kształtu, który nie ma) i będzie zaciemniać tylko to, czego naprawdę robi których jednocześnie odkrywa rodzaje tych dwóch kształtów, więc nie uważam tego za realistyczne rozwiązanie.

Jaskółka oknówka
źródło
1

Czasami nie chcesz dodawać wspólnej metody do wszystkich klas, ponieważ tak naprawdę to nie jest odpowiedzialny za wykonanie tego konkretnego zadania.

Na przykład chcesz narysować niektóre elementy, ale nie chcesz dodawać do nich kodu rysunku (co ma sens). W językach nieobsługujących wielokrotnej wysyłki możesz otrzymać następujący kod:

void DrawEntity(Entity entity) {
    if (entity instanceof Circle) {
        DrawCircle((Circle) entity));
    else if (entity instanceof Rectangle) {
        DrawRectangle((Rectangle) entity));
    } ...
}

Staje się to problematyczne, gdy ten kod pojawia się w kilku miejscach i trzeba go wszędzie modyfikować podczas dodawania nowego typu jednostki. W takim przypadku można tego uniknąć, stosując wzorzec użytkownika, ale czasem lepiej jest po prostu zachować prostotę i nie nadużytkować. Są to sytuacje, w których testowanie typu jest prawidłowe.

Honza Brabec
źródło
0

Używam tylko w połączeniu z refleksją. Ale nawet wtedy sprawdzanie dynamiczne jest przeważnie nie zakodowane na stałe w określonej klasie (lub tylko zakodowane na stałe w specjalnych klasach, takich jak Stringlub List).

Przez dynamiczne sprawdzanie mam na myśli:

boolean checkType(Type type, Object object) {
    if (object.isOfType(type)) {

    }
}

i nie zakodowane na stałe

boolean checkIsManaer(Object object) {
    if (object instanceof Manager) {

    }
}
m3th0dman
źródło
0

Testowanie typu i odlewanie typu to dwie ściśle ze sobą powiązane koncepcje. Tak ściśle powiązane, że jestem przekonany, że nigdy nie powinieneś przeprowadzać testu typu, chyba że masz zamiar wpisać rzut obiektu na podstawie wyniku.

Gdy myślisz o idealnym projektowaniu obiektowym, nigdy nie powinno się przeprowadzać testów typu (i rzutowania) . Ale mam nadzieję, że do tej pory zorientowałeś się, że programowanie obiektowe nie jest idealne. Czasami, szczególnie w przypadku kodu niższego poziomu, kod nie może pozostać wierny ideałowi. Tak jest w przypadku ArrayLists w Javie; ponieważ nie wiedzą w czasie wykonywania, jaka klasa jest przechowywana w tablicy, tworzą Object[]tablice i statycznie rzutują je na poprawny typ.

Zwrócono uwagę, że powszechna potrzeba testowania typu (i rzutowania) wynika z Equalsmetody, która w większości języków ma być prosta Object. Implementacja powinna mieć kilka szczegółowych kontroli, aby sprawdzić, czy dwa obiekty są tego samego typu, co wymaga możliwości przetestowania, jakiego typu są.

Badanie typu pojawia się również często w refleksji. Często masz metody, które zwracają Object[]lub inną ogólną tablicę i chcesz wyciągnąć wszystkie Fooobiekty z dowolnego powodu. Jest to całkowicie uzasadnione zastosowanie testowania typu i odlewania.

Ogólnie rzecz biorąc, testowanie typu jest złe, gdy niepotrzebnie łączy kod z tym, jak napisano określoną implementację. Może to łatwo prowadzić do konieczności przeprowadzenia konkretnego testu dla każdego typu lub kombinacji typów, na przykład jeśli chcesz znaleźć przecięcie linii, prostokątów i okręgów, a funkcja przecięcia ma inny algorytm dla każdej kombinacji. Twoim celem jest umieszczenie wszelkich szczegółów specyficznych dla jednego rodzaju obiektu w tym samym miejscu co ten obiekt, ponieważ ułatwi to utrzymanie i rozszerzenie twojego kodu.

meustrus
źródło
1
ArrayListsnie wiem, czy klasa jest przechowywana w środowisku wykonawczym, ponieważ Java nie miała generycznych, a kiedy zostały w końcu wprowadzone Oracle wybrało kompatybilność wsteczną z kodem bez generycznych. equalsma ten sam problem i i tak jest wątpliwą decyzją projektową; porównania równości nie mają sensu dla każdego typu.
Doval
1
Z technicznego punktu widzenia kolekcje Java nie przekazują swojej zawartości do niczego. Kompilator wstrzykuje typecasty w miejscu każdego dostępu: np.String x = (String) myListOfStrings.get(0)
Ostatnim razem, gdy patrzyłem na źródło Java (które mogło być 1.6 lub 1.5), było jawne rzutowanie w źródle ArrayList. Z dobrego powodu generuje (pomijane) ostrzeżenie kompilatora, ale i tak jest dozwolone. Przypuszczam, że można powiedzieć, że ze względu na to, jak zaimplementowano generyczne, zawsze i Objecttak było to możliwe; generics w Javie zapewnia tylko niejawne rzutowanie bezpieczne dzięki regułom kompilatora.
meustrus
0

Jest to dopuszczalne w przypadku, gdy musisz podjąć decyzję, która obejmuje dwa typy, a decyzja ta jest enkapsulowana w obiekcie spoza tej hierarchii typów. Załóżmy na przykład, że planujesz, który obiekt zostanie następnie przetworzony na liście obiektów oczekujących na przetworzenie:

abstract class Vehicle
{
    abstract void Process();
}

class Car : Vehicle { ... }
class Boat : Vehicle { ... }
class Truck : Vehicle { ... }

Powiedzmy teraz, że nasza logika biznesowa jest dosłownie „wszystkie samochody mają pierwszeństwo przed łodziami i ciężarówkami”. Dodanie Prioritywłaściwości do klasy nie pozwala na wyraźne wyrażenie logiki biznesowej, ponieważ skończy się na tym:

abstract class Vehicle
{
    abstract void Process();
    abstract int Priority { get }
}

class Car : Vehicle { public Priority { get { return 1; } } ... }
class Boat : Vehicle { public Priority { get { return 2; } } ... }
class Truck : Vehicle { public Priority { get { return 2; } } ... }

Problem polega na tym, że aby zrozumieć kolejność priorytetową, musisz spojrzeć na wszystkie podklasy lub innymi słowy, dodałeś sprzężenie do podklas.

Powinieneś oczywiście przekształcić priorytety w stałe i umieścić je w klasie samodzielnie, co pomaga utrzymać wspólne planowanie logiki biznesowej:

static class Priorities
{
    public const int CAR_PRIORITY = 1;
    public const int BOAT_PRIORITY = 2;
    public const int TRUCK_PRIORITY = 2;
}

Jednak w rzeczywistości algorytm szeregowania może się zmienić w przyszłości i ostatecznie może zależeć od więcej niż tylko typu. Na przykład może powiedzieć „ciężarówki o masie 5000 kg mają szczególny priorytet w stosunku do wszystkich innych pojazdów”. Dlatego algorytm szeregowania należy do własnej klasy i dobrym pomysłem jest sprawdzenie typu, aby ustalić, który z nich powinien być pierwszy:

class VehicleScheduler : IScheduleVehicles
{
    public Vehicle WhichVehicleGoesFirst(Vehicle vehicle1, Vehicle vehicle2)
    {
        if(vehicle1 is Car) return vehicle1;
        if(vehicle2 is Car) return vehicle2;
        return vehicle1;
    }
}

Jest to najprostszy sposób na wdrożenie logiki biznesowej i jednocześnie najbardziej elastyczny na przyszłe zmiany.

Scott Whitlock
źródło
Nie zgadzam się z twoim konkretnym przykładem, ale zgodziłbym się z zasadą posiadania prywatnego odniesienia, które może zawierać różne typy o różnych znaczeniach. Jako lepszy przykład zasugerowałbym pole, które może pomieścić null, a Stringlub a String[]. Jeśli 99% obiektów będzie potrzebowało dokładnie jednego łańcucha, kapsułkowanie każdego łańcucha w osobno skonstruowanej String[]może spowodować znaczne obciążenie pamięci. Obsługa przypadku pojedynczego ciągu przy użyciu bezpośredniego odwołania do a Stringbędzie wymagać więcej kodu, ale pozwoli zaoszczędzić miejsce na dysku i może przyspieszyć działanie.
supercat
0

Testowanie typu jest narzędziem, używaj go mądrze i może być potężnym sojusznikiem. Użyj go źle, a twój kod zacznie wąchać.

W naszym oprogramowaniu otrzymywaliśmy wiadomości przez sieć w odpowiedzi na zapytania. Wszystkie zdserializowane wiadomości miały wspólną klasę podstawową Message.

Same klasy były bardzo proste, tylko ładunek jako wpisane właściwości C # i procedury ich rozbierania i rozszyfrowywania (w rzeczywistości generowałem większość klas przy użyciu szablonów t4 z opisu XML formatu komunikatu)

Kod mógłby wyglądać następująco:

Message response = await PerformSomeRequest(requestParameter);

// Server (out of our control) would send one message as response, but 
// the actual message type is not known ahead of time (it depended on 
// the exact request and the state of the server etc.)
if (response is ErrorMessage)
{ 
    // Extract error message and pass along (for example using exceptions)
}
else if (response is StuffHappenedMessage)
{
    // Extract results
}
else if (response is AnotherThingHappenedMessage)
{
    // Extract another type of result
}
// Half a dozen other possible checks for messages

To prawda, że ​​można argumentować, że architektura komunikatu może być lepiej zaprojektowana, ale została zaprojektowana dawno temu, a nie dla C #, więc taka jest. Tutaj testy typu rozwiązały dla nas prawdziwy problem w niezbyt odrapany sposób.

Warto zauważyć, że C # 7.0 dostaje dopasowanie wzorca (co pod wieloma względami jest testowaniem typu na sterydach), nie może być wszystko źle ...

Isak Savo
źródło
0

Weź ogólny parser JSON. Wynikiem pomyślnej analizy jest tablica, słownik, ciąg, liczba, wartość logiczna lub wartość null. Może to być dowolny z nich. A elementy tablicy lub wartości w słowniku mogą ponownie być dowolnymi z tych typów. Ponieważ dane te pochodzą spoza programu, trzeba zaakceptować żadnego rezultatu (czyli trzeba przyjąć ją bez upaść, ty może odrzucić wynik, który nie jest tym, czego się spodziewać).

gnasher729
źródło
Cóż, niektóre deserializatory JSON spróbują utworzyć instancję drzewa obiektów, jeśli twoja struktura zawiera informacje o typie dla „pól wnioskowania”. Ale tak. Myślę, że w tym kierunku zmierza odpowiedź Karla B.
svidgen,