Pochodzę z języka C # i Java, jestem przyzwyczajony do tego, że moje listy są jednorodne i to ma dla mnie sens. Kiedy zacząłem zbierać Lisp, zauważyłem, że listy mogą być niejednorodne. Kiedy zacząłem przekręcać ze dynamic
słowem kluczowym w C #, zauważyłem, że od C # 4.0 mogą istnieć również listy heterogeniczne:
List<dynamic> heterogeneousList
Moje pytanie brzmi o co chodzi? Wydaje się, że lista heterogeniczna będzie miała znacznie większy narzut podczas przetwarzania, a jeśli chcesz przechowywać różne typy w jednym miejscu, możesz potrzebować innej struktury danych. Czy moja naiwność wychowuje brzydką twarz, czy naprawdę są chwile, kiedy warto mieć heterogeniczną listę?
List<dynamic>
różni się (w przypadku twojego pytania) od zwykłego działaniaList<object>
?Odpowiedzi:
Artykuł „ Silnie wpisane zbiory heterogeniczne” Olega Kiselyova, Ralfa Lämmela i Keeana Schupke zawiera nie tylko implementację list heterogenicznych w Haskell, ale także motywujący przykład tego, kiedy, dlaczego i jak korzystać z list HLists. W szczególności używają go do bezpiecznego dostępu do bazy danych sprawdzonej podczas kompilacji. (Pomyśl, LINQ, w rzeczywistości, do którego się odnoszą, jest artykuł Haskella autorstwa Erika Meijera i innych, który doprowadził do LINQ.)
Cytując z wprowadzającego akapitu artykułu HLists:
Zauważ, że przykłady, które podałeś w swoim pytaniu, tak naprawdę nie są listami heterogenicznymi w tym sensie, że słowo to jest powszechnie używane. Są to listy o słabym typie lub bez typu . W rzeczywistości są one w rzeczywistości homogenicznymi listami, ponieważ wszystkie elementy są tego samego typu:
object
lubdynamic
. Następnie musisz wykonać rzutowania lub niesprawdzoneinstanceof
testy lub coś w tym rodzaju, aby faktycznie móc znacząco pracować z elementami, co powoduje, że są one słabo wpisane.źródło
Krótko mówiąc, heterogeniczne pojemniki wymieniają wydajność środowiska wykonawczego w celu zapewnienia elastyczności. Jeśli chcesz mieć „listę rzeczy” bez względu na konkretny rodzaj rzeczy, heterogeniczność jest dobrym rozwiązaniem. Lispy są typowo dynamicznie typowane, a większość wszystkiego i tak jest listą wad pudełkowych wartości, więc oczekuje się niewielkiego spadku wydajności. W świecie Lisp wydajność programisty jest uważana za ważniejszą niż wydajność środowiska wykonawczego.
W języku dynamicznie typowanym jednorodne kontenery faktycznie miałyby niewielki narzut w porównaniu do kontenerów heterogenicznych, ponieważ wszystkie dodane elementy musiałyby zostać sprawdzone pod względem typu.
Twoja intuicja w wyborze lepszej struktury danych jest na miejscu. Ogólnie rzecz biorąc, im więcej umów możesz zawrzeć w kodzie, tym więcej wiesz o tym, jak on działa, i tym bardziej niezawodny, łatwy w utrzymaniu i c. staje się. Czasami jednak naprawdę potrzebujesz heterogenicznego pojemnika i powinieneś mieć możliwość posiadania takiego, jeśli go potrzebujesz.
źródło
IUserSetting
i zaimplementować go kilkakrotnie lub stworzyć ogólnyUserSetting<T>
, ale jednym z problemów ze statycznym pisaniem jest to, że definiujesz interfejs, zanim dokładnie wiesz, jak będzie on używany. Rzeczy, które robisz z ustawieniami liczb całkowitych, prawdopodobnie bardzo różnią się od rzeczy, które robisz z ustawieniami ciągów, więc jakie operacje mają sens, aby umieścić we wspólnym interfejsie? Dopóki się nie upewnisz, lepiej rozsądnie używać dynamicznego pisania i uczynić go konkretnym później.object
raczej niż adynamic
, to na pewno skorzystaj z pierwszego.W językach funkcjonalnych (takich jak lisp) używasz dopasowania wzorca, aby określić, co dzieje się z określonym elementem na liście. Odpowiednikiem w języku C # byłby łańcuch instrukcji if ... elseif, które sprawdzają typ elementu i wykonują na tej podstawie operację. Nie trzeba dodawać, że funkcjonalne dopasowanie wzorca jest bardziej wydajne niż sprawdzanie typu środowiska wykonawczego.
Zastosowanie polimorfizmu byłoby bliższe dopasowaniu do dopasowania wzorca. Oznacza to, że dopasowanie obiektów listy do określonego interfejsu i wywołanie funkcji w tym interfejsie dla każdego obiektu. Inną alternatywą byłoby zapewnienie serii przeciążonych metod, które przyjmują określony typ obiektu jako parametr. Domyślna metoda przyjmująca Object jako parametr.
Takie podejście zapewnia przybliżenie dopasowania wzorca Lisp. Wzorzec odwiedzających (jak tu zaimplementowano, jest doskonałym przykładem użycia list heterogenicznych). Innym przykładem może być wysyłanie wiadomości, w której są nasłuchiwania określonych wiadomości w kolejce priorytetowej i przy użyciu łańcucha odpowiedzialności, dyspozytor przekazuje wiadomość, a pierwszy moduł obsługi, który pasuje do wiadomości, obsługuje ją.
Odwrotna strona powiadamia wszystkich, którzy zarejestrują się na wiadomość (na przykład wzorzec agregatora zdarzeń powszechnie używany do luźnego łączenia ViewModels we wzorcu MVVM). Używam następującej konstrukcji
Jedynym sposobem na dodanie do słownika jest funkcja
(a obiekt jest w rzeczywistości słabym odniesieniem do przekazywanego programu obsługi). Więc muszę użyć List <Object>, ponieważ w czasie kompilacji nie wiem, jaki będzie typ zamknięty. Jednak w Runtime mogę wymusić, że to ten typ jest kluczem do słownika. Kiedy chcę odpalić wydarzenie, wzywam
i ponownie rozwiązuję listę. Nie ma żadnej korzyści z używania Listy <dynamicznej>, ponieważ i tak muszę ją rzucić. Jak widać, zalety obu podejść są zasadne. Jeśli zamierzasz dynamicznie wywołać obiekt za pomocą przeciążenia metody, dynamika jest na to sposobem. Jeśli jesteś zmuszony do rzucania niezależnie, równie dobrze możesz użyć Object.
źródło
DoSomething(Object)
(przynajmniej podczas używaniaobject
wforeach
pętli;dynamic
to zupełnie inna sprawa).Masz rację, że niejednorodność pociąga za sobą czas działania, ale co ważniejsze, osłabia gwarancje czasu kompilacji zapewniane przez moduł sprawdzania typów. Mimo to istnieją pewne problemy, w których alternatywy są jeszcze bardziej kosztowne.
Z mojego doświadczenia wynika, że przy przetwarzaniu nieprzetworzonych bajtów przez pliki, gniazda sieciowe itp. Często napotykasz takie problemy.
Aby dać prawdziwy przykład, rozważ system obliczeń rozproszonych z wykorzystaniem kontraktów futures . Pracownik w pojedynczym węźle może spawnować pracę dowolnego typu możliwego do serializacji, co daje przyszłość tego typu. Za kulisami system wysyła pracę do peera, a następnie zapisuje zapis powiązania tej jednostki pracy z daną przyszłością, którą należy wypełnić po powrocie odpowiedzi na tę pracę.
Gdzie można przechowywać te zapisy? Intuicyjnie chcesz coś takiego
Dictionary<WorkId, Future<TValue>>
, ale ogranicza to zarządzanie tylko jednym rodzajem kontraktów terminowych w całym systemie. Bardziej odpowiedni jest typDictionary<WorkId, Future<dynamic>>
, ponieważ pracownik może rzucić na odpowiedni typ, gdy wymusza przyszłość.Uwaga : ten przykład pochodzi ze świata Haskell, w którym nie mamy podtytułów. Nie zdziwiłbym się, gdyby istniało bardziej idiomatyczne rozwiązanie dla tego konkretnego przykładu w języku C #, ale mam nadzieję, że nadal jest ilustracyjne.
źródło
ISTR, że Lisp nie ma żadnych struktur danych innych niż lista, więc jeśli potrzebujesz dowolnego typu agregowanego obiektu danych, będzie to musiała być lista heterogeniczna. Jak zauważyli inni, są one również przydatne do szeregowania danych w celu transmisji lub przechowywania. Jedną z fajnych cech jest to, że są one również otwarte, dzięki czemu można ich używać w systemie opartym na analogii potoków i filtrów, a także zwiększać kolejne etapy przetwarzania lub poprawiać dane bez konieczności posiadania stałego obiektu danych lub topologii przepływu pracy .
źródło