Generalnie unikam rzutowania typów tak bardzo, jak to możliwe, ponieważ mam wrażenie, że jest to zła praktyka kodowania i może spowodować spadek wydajności.
Ale gdyby ktoś poprosił mnie o wyjaśnienie, dlaczego dokładnie tak jest, prawdopodobnie patrzyłbym na nich jak jeleń w reflektorach.
Więc dlaczego / kiedy casting jest zły?
Czy jest to ogólne rozwiązanie dla języków Java, C #, C ++, czy też każde inne środowisko wykonawcze radzi sobie z nim na własnych warunkach?
Mile widziane są specyfikacje dla dowolnego języka, na przykład dlaczego jest zły w języku c ++?
Odpowiedzi:
Oznaczyłeś to trzema językami, a odpowiedzi są bardzo różne w tych trzech. Dyskusja o C ++ mniej lub bardziej implikuje również dyskusję o rzutach C, co daje (mniej więcej) czwartą odpowiedź.
Ponieważ jest to ten, o którym nie wspomniałeś wprost, zacznę od C. Rzuty C mają wiele problemów. Jednym z nich jest to, że mogą robić wiele różnych rzeczy. W niektórych przypadkach rzutowanie nie robi nic poza informacją kompilatora (w zasadzie): „zamknij się, wiem, co robię” - tj. Zapewnia, że nawet gdy wykonujesz konwersję, która może powodować problemy, kompilator nie ostrzeże Cię o tych potencjalnych problemach. Na przykład
char a=(char)123456;
. Dokładny wynik tej implementacji zdefiniowany (zależy od rozmiaru i podpisuchar
) iz wyjątkiem dość dziwnych sytuacji, prawdopodobnie nie jest przydatny. Rzuty C różnią się również tym, czy są czymś, co dzieje się tylko w czasie kompilacji (tj. Po prostu mówisz kompilatorowi, jak interpretować / traktować niektóre dane), czy czymś, co dzieje się w czasie wykonywania (np. Rzeczywista konwersja z double do długie).C ++ próbuje sobie z tym poradzić, przynajmniej do pewnego stopnia, dodając kilka „nowych” operatorów rzutowania, z których każdy jest ograniczony tylko do podzbioru możliwości rzutowania w C. Utrudnia to (na przykład) przypadkowe wykonanie konwersji, której tak naprawdę nie zamierzałeś - jeśli zamierzasz tylko odrzucić stałość na obiekcie, możesz użyć
const_cast
i mieć pewność, że jedyną rzeczą, na którą może wpłynąć, jest to, czy celem jestconst
,volatile
albo nie. I odwrotnie, astatic_cast
nie może wpływać na to, czy obiekt jestconst
czyvolatile
. Krótko mówiąc, masz większość tych samych typów możliwości, ale są one podzielone na kategorie, więc jedno rzutowanie może generalnie wykonać tylko jeden rodzaj konwersji, gdzie pojedyncze rzutowanie w stylu C może wykonać dwie lub trzy konwersje w jednej operacji. Podstawowym wyjątkiem jest to, że możesz użyćdynamic_cast
zamiast astatic_cast
przynajmniej w niektórych przypadkach i mimo że zostanie zapisane jako adynamic_cast
, tak naprawdę skończy się jakostatic_cast
. Na przykład, możesz użyćdynamic_cast
do przechodzenia w górę lub w dół hierarchii klas - ale rzutowanie „w górę” hierarchii jest zawsze bezpieczne, więc można to zrobić statycznie, podczas gdy rzutowanie „w dół” hierarchii niekoniecznie jest bezpieczne, więc wykonywane dynamicznie.Java i C # są do siebie znacznie bardziej podobne. W szczególności, w przypadku obu z nich rzutowanie jest (praktycznie?) Zawsze operacją wykonywaną. Jeśli chodzi o operatory rzutowania C ++, jest zwykle najbliżej a
dynamic_cast
pod względem tego, co naprawdę zostało zrobione - tj. Kiedy próbujesz rzutować obiekt na jakiś typ docelowy, kompilator wstawia sprawdzenie w czasie wykonywania, aby zobaczyć, czy ta konwersja jest dozwolona i zgłoś wyjątek, jeśli tak nie jest. Dokładne szczegóły (np. Nazwa użyta dla wyjątku „złe rzucanie”) są różne, ale podstawowa zasada pozostaje w większości podobna (chociaż, jeśli pamięć służy, Java sprawia, że rzutowania są stosowane do kilku typów niebędących obiektami, takich jakint
znacznie bliższe C rzuty - ale te typy są używane na tyle rzadko, że 1) nie pamiętam tego na pewno i 2) nawet jeśli to prawda, i tak nie ma to większego znaczenia).Patrząc na rzeczy bardziej ogólnie, sytuacja jest dość prosta (przynajmniej IMO): rzutowanie (oczywiście) oznacza, że konwertujesz coś z jednego typu na inny. Kiedy / jeśli to zrobisz, pojawi się pytanie „Dlaczego?” Jeśli naprawdę chcesz, aby coś było określonym typem, dlaczego nie zdefiniowałeś tego jako tego typu? Nie oznacza to, że nigdy nie ma powodu, aby wykonywać taką konwersję, ale za każdym razem, gdy to nastąpi, powinno nasunąć pytanie, czy można przeprojektować kod, tak aby cały czas był używany prawidłowy typ. Nawet pozornie nieszkodliwe konwersje (np. Między liczbami całkowitymi a zmiennoprzecinkowymi) powinny być zbadane znacznie dokładniej niż jest to powszechne. Pomimo ich pozorupodobieństwo, liczby całkowite powinny być naprawdę używane dla „policzonych” typów rzeczy, a zmiennoprzecinkowe dla „mierzonych” rodzajów rzeczy. Ignorowanie tego rozróżnienia prowadzi do niektórych szalonych stwierdzeń, takich jak „przeciętna amerykańska rodzina ma 1,8 dzieci”. Chociaż wszyscy możemy zobaczyć, jak to się dzieje, faktem jest, że żadna rodzina nie ma 1,8 dzieci. Mogą mieć 1, 2 lub więcej, ale nigdy 1,8.
źródło
Wiele dobrych odpowiedzi. Oto jak na to patrzę (z perspektywy C #).
Przesyłanie zwykle oznacza jedną z dwóch rzeczy:
Znam typ środowiska wykonawczego tego wyrażenia, ale kompilator go nie zna. Kompilator, mówię ci, w czasie wykonywania obiekt, który odpowiada temu wyrażeniu, naprawdę będzie tego typu. Na razie wiesz, że to wyrażenie należy traktować jako tego typu. Wygeneruj kod, który zakłada, że obiekt będzie danego typu lub wyrzuć wyjątek, jeśli się mylę.
Zarówno kompilator, jak i programista znają typ środowiska wykonawczego wyrażenia. Istnieje inna wartość innego typu skojarzona z wartością, którą to wyrażenie będzie miało w czasie wykonywania. Generuj kod, który tworzy wartość żądanego typu z wartości danego typu; jeśli nie możesz tego zrobić, zgłoś wyjątek.
Zauważ, że to są przeciwieństwa . Istnieją dwa rodzaje odlewów! Istnieją rzuty, w których dajesz kompilatorowi wskazówkę dotyczącą rzeczywistości - hej, ten obiekt typu jest w rzeczywistości typu Customer - i są rzuty, w których mówisz kompilatorowi, aby wykonał mapowanie z jednego typu na inny - hej, Potrzebuję int, który odpowiada temu podwójnemu.
Oba rodzaje rzutów to czerwone flagi. Pierwszy rodzaj rzutowania rodzi pytanie "dlaczego dokładnie programista wie coś, czego nie ma kompilator?" Jeśli jesteś w takiej sytuacji, to lepiej zrobić to zwykle zmienić program tak, że kompilator nie posiada uchwyt na rzeczywistość. Wtedy nie potrzebujesz obsady; analiza jest wykonywana w czasie kompilacji.
Drugi rodzaj rzutowania rodzi pytanie „dlaczego operacja nie jest wykonywana w pierwszej kolejności w docelowym typie danych?” Jeśli potrzebujesz wyniku w int, to dlaczego w pierwszej kolejności trzymasz dubla? Nie powinieneś trzymać int?
Kilka dodatkowych myśli:
http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/
źródło
Foo
obiekt, który dziedziczy po nimBar
i przechowujesz go w aList<Bar>
, będziesz potrzebować rzutów, jeśli chcesz to zFoo
powrotem. Być może wskazuje to na problem na poziomie architektonicznym (dlaczego przechowujemyBar
s zamiastFoo
s?), Ale niekoniecznie. A jeśliFoo
ma to również prawidłową obsadęint
, dotyczy to również Twojego innego komentarza: Przechowujesz aFoo
, a nie anint
, ponieważint
nie zawsze jest odpowiednie.Foo
w aList<Bar>
, ale w miejscu obsady próbujesz zrobić coś,Foo
co nie jest odpowiednie dlaBar
. Oznacza to, że różne zachowania dla podtypów są wykonywane przez mechanizm inny niż wbudowany polimorfizm zapewniany przez metody wirtualne. Może to dobre rozwiązanie, ale częściej jest to czerwona flaga.Tuple<X,Y,...>
krotek jest mało prawdopodobne, aby zobaczył powszechne zastosowanie w C #. Jest to jedyne miejsce, w którym język mógłby skuteczniej popychać ludzi do „dołu sukcesu”.Błędy przesyłania są zawsze zgłaszane jako błędy czasu wykonywania w języku Java. Używanie typów ogólnych lub szablonów zmienia te błędy w błędy w czasie kompilacji, co znacznie ułatwia wykrycie błędu.
Jak powiedziałem powyżej. Nie oznacza to, że wszystkie castingi są złe. Ale jeśli można tego uniknąć, najlepiej to zrobić.
źródło
Casting nie jest z natury zły, po prostu często jest nadużywany jako środek do osiągnięcia czegoś, czego naprawdę nie powinno się robić w ogóle lub robić bardziej elegancko.
Gdyby był ogólnie zły, języki by go nie wspierały. Jak każda inna funkcja językowa ma swoje miejsce.
Moją radą byłoby skupienie się na swoim podstawowym języku i zrozumienie wszystkich jego obsady oraz powiązanych najlepszych praktyk. To powinno informować wycieczki w innych językach.
Odpowiednie dokumenty C # są tutaj .
W poprzednim pytaniu SO znajduje się świetne podsumowanie opcji C ++ .
źródło
Mówię tutaj głównie o C ++ , ale większość z tego prawdopodobnie dotyczy również Java i C #:
C ++ jest językiem z typowaniem statycznym . Istnieją pewne możliwości, na jakie pozwala język (funkcje wirtualne, niejawne konwersje), ale w zasadzie kompilator zna typ każdego obiektu w czasie kompilacji. Powodem używania takiego języka jest to, że błędy można wykryć w czasie kompilacji . Jeśli kompilator zna typy
a
ib
, złapie Cię w czasie kompilacji, gdy zrobisza=b
gdziea
jest liczbą zespoloną ib
ciągiem.Ilekroć wykonujesz rzutowanie jawne, mówisz kompilatorowi, aby się zamknął, ponieważ myślisz, że wiesz lepiej . Jeśli się mylisz, zwykle dowiesz się o tym dopiero w czasie wykonywania . Problem ze stwierdzeniem w czasie wykonywania polega na tym, że może to być u klienta.
źródło
Java, c # i c ++ są językami silnie wpisanymi typami, chociaż języki silnie typowane mogą być postrzegane jako nieelastyczne, mają tę zaletę, że sprawdzają typ w czasie kompilacji i chronią przed błędami w czasie wykonywania, które są spowodowane niewłaściwym typem dla niektórych operacji.
Istnieją zasadniczo dwa rodzaje rzutów: rzut do bardziej ogólnego typu lub rzut do innego typu (bardziej szczegółowy). Rzutowanie na typ bardziej ogólny (rzutowanie na typ nadrzędny) pozostawi kontrolę czasu kompilacji nienaruszoną. Jednak rzutowanie na inne typy (bardziej szczegółowe typy) wyłączy sprawdzanie typów w czasie kompilacji i zostanie zastąpione przez kompilator sprawdzeniem w czasie wykonywania. Oznacza to, że masz mniejszą pewność, że skompilowany kod będzie działał poprawnie. Ma również niewielki wpływ na wydajność, ze względu na dodatkowe sprawdzanie typu w czasie wykonywania (API Java jest pełne rzutów!).
źródło
dynamic_cast
jest generalnie wolniejszy niżstatic_cast
, ponieważ generalnie musi sprawdzać typ w czasie wykonywania (z pewnymi zastrzeżeniami: rzutowanie na typ podstawowy jest tanie itp.).Niektóre rodzaje rzutów są tak bezpieczne i wydajne, że często w ogóle nie można ich uznać za rzucanie.
Jeśli rzutujesz z typu pochodnego na typ podstawowy, jest to generalnie dość tanie (często - w zależności od języka, implementacji i innych czynników - jest zerowe) i bezpieczne.
Jeśli rzucasz z prostego typu, takiego jak int, do szerszego typu, takiego jak long int, to znowu jest to dość tanie (generalnie niewiele droższe niż przypisanie tego samego typu, do którego jest rzutowany) i znowu jest bezpieczne.
Inne typy są bardziej najeżone i / lub droższe. W większości języków rzutowanie z typu podstawowego do typu pochodnego jest albo tanie, ale wiąże się z wysokim ryzykiem poważnego błędu (w C ++, jeśli static_cast z bazowego na pochodny, będzie to tanie, ale jeśli wartość bazowa nie jest typu pochodnego, zachowanie jest niezdefiniowane i może być bardzo dziwne) lub stosunkowo drogie i wiąże się z ryzykiem wywołania wyjątku (dynamic_cast w C ++, jawne rzutowanie z bazy na pochodne w C # i tak dalej). Boksowanie w Javie i C # to kolejny tego przykład i jeszcze większy koszt (biorąc pod uwagę, że zmieniają się one bardziej niż tylko sposób traktowania bazowych wartości).
Inne typy rzutowania mogą spowodować utratę informacji (długi typ całkowity na krótki typ całkowity).
Te przypadki ryzyka (wyjątek lub poważniejszy błąd) i kosztów są powodem, dla którego należy unikać rzucania.
Bardziej konceptualnym, ale być może ważniejszym powodem jest to, że każdy przypadek rzutowania jest przypadkiem, w którym twoja zdolność rozumowania poprawności kodu jest utrudniona: każdy przypadek to inne miejsce, w którym coś może pójść nie tak i sposoby, w jakie może się nie udać, co dodatkowo komplikuje wnioskowanie, czy system jako całość się nie powiedzie. Nawet jeśli obsada jest za każdym razem pewnie bezpieczna, udowodnienie tego jest dodatkową częścią rozumowania.
Wreszcie, intensywne użycie rzutów może wskazywać na niepowodzenie we właściwym rozważeniu modelu obiektowego podczas jego tworzenia, używania lub obu tych rzeczy: Rzutowanie w przód iw tył między tymi samymi kilkoma typami często prawie zawsze kończy się niepowodzeniem w rozważeniu relacji między typami używany. Tutaj nie chodzi o to, że odlewy są złe, ale są oznaką czegoś złego.
źródło
Programiści mają coraz większą tendencję do trzymania się dogmatycznych zasad dotyczących używania funkcji językowych („nigdy nie używaj XXX!”, „XXX uważane za szkodliwe” itp.), Gdzie XXX waha się od
goto
s do wskaźników doprotected
członków danych, pojedynczych do przechodzących obiektów wartość.Z mojego doświadczenia wynika, że przestrzeganie takich zasad zapewnia dwie rzeczy: nie będziesz ani złym programistą, ani świetnym programistą.
O wiele lepszym rozwiązaniem jest wykopać dół i odkryć ziarno prawdy za tymi zakazami koc, a następnie użyj funkcji rozważnie, ze zrozumieniem, że tam są w wielu sytuacjach, dla których są one najlepszym narzędziem do pracy.
„Generalnie unikam rzucania typów tak bardzo, jak to możliwe” jest dobrym przykładem takiej nadmiernie uogólnionej reguły. Gipsy są niezbędne w wielu typowych sytuacjach. Kilka przykładów:
typedef
). (Przykład:GLfloat
<-->double
<-->Real
.)std::size_type
->int
.)Z pewnością jest wiele sytuacji, w których użycie obsady nie jest właściwe i ważne jest, aby się ich nauczyć; Nie będę wchodził w zbyt wiele szczegółów, ponieważ powyższe odpowiedzi wykonały dobrą robotę, wskazując niektóre z nich.
źródło
Aby rozwinąć odpowiedź KDevelopera , nie jest ona z natury bezpieczna dla typów. W przypadku przesyłania nie ma gwarancji, że to, z czego przesyłasz i do czego, będzie pasować, a jeśli tak się stanie, otrzymasz wyjątek czasu wykonywania, co zawsze jest złe.
W odniesieniu do języka C #, ponieważ zawiera on operatory
is
ias
, masz możliwość (w większości) określenia, czy rzutowanie powiedzie się. Z tego powodu należy podjąć odpowiednie kroki, aby określić, czy operacja zakończy się powodzeniem, i postępować prawidłowo.źródło
W przypadku języka C # należy być bardziej ostrożnym podczas rzutowania ze względu na narzuty związane z pakowaniem / rozpakowywaniem podczas zajmowania się typami wartości.
źródło
Nie jestem pewien, czy ktoś już o tym wspomniał, ale w C # rzutowanie może być używane w dość bezpieczny sposób i często jest konieczne. Załóżmy, że otrzymujesz obiekt, który może być kilku typów. Używając
is
słowa kluczowego, możesz najpierw potwierdzić, że obiekt jest rzeczywiście tego typu, na który chcesz go rzutować, a następnie bezpośrednio rzutować obiekt na ten typ. (Nie pracowałem dużo z Javą, ale jestem pewien, że jest na to również bardzo prosty sposób).źródło
Obiekt jest rzutowany na pewien typ tylko wtedy, gdy spełnione są 2 warunki:
Oznacza to, że nie wszystkie posiadane informacje są dobrze reprezentowane w używanej strukturze typów. To jest złe, ponieważ twoja implementacja powinna być semantyczna zawierać twój model, czego wyraźnie nie ma w tym przypadku.
Teraz, kiedy wykonujesz rzut, może to mieć 2 różne powody:
W większości języków często napotykasz drugą sytuację. Generics jak w Javie trochę pomagają, system szablonów C ++ jeszcze bardziej, ale jest trudny do opanowania i nawet wtedy niektóre rzeczy mogą być niemożliwe lub po prostu nie warte wysiłku.
Można więc powiedzieć, że obsada to brudny hack mający na celu obejście twoich problemów i wyrażenie określonej relacji między typami w jakimś konkretnym języku. Należy unikać brudnych hacków. Ale bez nich nie możesz żyć.
źródło
Ogólnie szablony (lub typy ogólne) są bardziej bezpieczne dla typów niż rzutowania. W związku z tym powiedziałbym, że problemem związanym z odlewaniem jest bezpieczeństwo typu. Jest jednak inna, bardziej subtelna kwestia związana zwłaszcza z poniżaniem: projekt. Przynajmniej z mojej perspektywy depresja to zapach kodu, wskazanie, że coś może być nie tak z moim projektem i powinienem to zbadać dalej. Dlaczego jest proste: jeśli dobrze „zrozumiesz” abstrakcje, po prostu ich nie potrzebujesz! Przy okazji fajne pytanie ...
Twoje zdrowie!
źródło
Aby być naprawdę zwięzłym, dobrym powodem jest przenośność. Różne architektury, które obsługują ten sam język, mogą mieć, powiedzmy, różne rozmiary int. Jeśli więc przeprowadzę migrację z ArchA do ArchB, który ma węższe int, w najlepszym razie mogę zobaczyć dziwne zachowanie, a w najgorszym - błędy seg.
(Wyraźnie ignoruję kod bajtowy niezależny od architektury i IL.)
źródło