TL; DR Przewiń na sam dół.
Z tego, co widzę, wdrażasz nowy język na C #. Wyliczenia wydają się oznaczać typ identyfikatora (lub cokolwiek, co ma nazwę i które pojawia się w kodzie źródłowym nowego języka), który wydaje się być stosowany do węzłów, które mają zostać dodane do drzewnej reprezentacji programu.
W tej szczególnej sytuacji istnieje bardzo niewiele zachowań polimorficznych między różnymi typami węzłów. Innymi słowy, chociaż konieczne jest, aby drzewo mogło zawierać węzły bardzo różnych typów (wariantów), faktyczna wizyta tych węzłów będzie zasadniczo polegać na gigantycznym łańcuchu „jeśli to wtedy” (lub instanceof
/ is
czekach). Te gigantyczne kontrole prawdopodobnie będą miały miejsce w wielu różnych miejscach projektu. To jest powód, dla którego wyliczenia mogą wydawać się pomocne, a przynajmniej są tak pomocne jak instanceof
/ is
czek.
Wzorzec odwiedzających może być nadal przydatny. Innymi słowy, istnieją różne style kodowania, których można użyć zamiast gigantycznego łańcucha instanceof
. Jeśli jednak chcesz omówić różne zalety i wady, wolisz zaprezentować przykład kodu z najbrzydszego łańcucha instanceof
w projekcie, zamiast spierać się o wyliczenia.
Nie oznacza to, że klasy i hierarchia dziedziczenia nie są przydatne. Wręcz przeciwnie. Chociaż nie ma żadnych zachowań polimorficznych, które działałyby dla każdego typu deklaracji (poza tym, że każda deklaracja musi mieć Name
właściwość), istnieje wiele bogatych zachowań polimorficznych wspólnych dla pobliskiego rodzeństwa. Na przykład Function
i Procedure
prawdopodobnie dzielą niektóre zachowania (oba są wywoływalne i akceptują listę wpisanych argumentów wejściowych) i na PropertyGet
pewno odziedziczą zachowania Function
(oba mają a ReturnType
). Możesz użyć wyliczeń lub kontroli dziedziczenia dla gigantycznego łańcucha „jeśli-to-inaczej”, ale zachowania polimorficzne, jakkolwiek fragmentaryczne, muszą być nadal implementowane w klasach.
Istnieje wiele internetowych porad dotyczących nadużywania instanceof
/ is
kontroli. Wydajność nie jest jednym z powodów. Powodem jest raczej uniemożliwienie programistom organicznego odkrycia odpowiednich zachowań polimorficznych, tak jakby instanceof
/ is
był kulą. Ale w twojej sytuacji nie masz innego wyboru, ponieważ te węzły mają bardzo niewiele wspólnego.
Oto kilka konkretnych sugestii.
Istnieje kilka sposobów reprezentowania grup nie będących liśćmi.
Porównaj następujący fragment oryginalnego kodu ...
[Flags]
public enum DeclarationType
{
Member = 1 << 7,
Procedure = 1 << 8 | Member,
Function = 1 << 9 | Member,
Property = 1 << 10 | Member,
PropertyGet = 1 << 11 | Property | Function,
PropertyLet = 1 << 12 | Property | Procedure,
PropertySet = 1 << 13 | Property | Procedure,
LibraryFunction = 1 << 23 | Function,
LibraryProcedure = 1 << 24 | Procedure,
}
do tej zmodyfikowanej wersji:
[Flags]
public enum DeclarationType
{
Nothing = 0, // to facilitate bit testing
// Let's assume Member is not a concrete thing,
// which means it doesn't need its own bit
/* Member = 1 << 7, */
// Procedure and Function are concrete things; meanwhile
// they can still have sub-types.
Procedure = 1 << 8,
Function = 1 << 9,
Property = 1 << 10,
PropertyGet = 1 << 11,
PropertyLet = 1 << 12,
PropertySet = 1 << 13,
LibraryFunction = 1 << 23,
LibraryProcedure = 1 << 24,
// new
Procedures = Procedure | PropertyLet | PropertySet | LibraryProcedure,
Functions = Function | PropertyGet | LibraryFunction,
Properties = PropertyGet | PropertyLet | PropertySet,
Members = Procedures | Functions | Properties,
LibraryMembers = LibraryFunction | LibraryProcedure
}
Ta zmodyfikowana wersja unika przydzielania bitów do nieprecyzyjnych typów deklaracji. Zamiast tego nieokreślone typy deklaracji (abstrakcyjne grupy typów deklaracji) mają po prostu wartości wyliczeniowe, które są bitowe lub (suma bitów) wszystkich swoich potomków.
Jest zastrzeżenie: jeśli istnieje abstrakcyjny typ deklaracji, który ma jedno dziecko, a jeśli istnieje potrzeba rozróżnienia między abstrakcyjnym (rodzicem) a konkretnym (dzieckiem), wówczas abstrakcyjny nadal będzie potrzebował własnego bitu .
Jedno zastrzeżenie, które jest specyficzne dla tego pytania: a Property
jest początkowo identyfikatorem (gdy widzisz tylko jego nazwę, nie widząc, jak jest używany w kodzie), ale może przekształcić się w PropertyGet
/ PropertyLet
/, PropertySet
jak tylko zobaczysz, jak jest używany w kodzie. Innymi słowy, na różnych etapach analizy może być konieczne oznaczenie Property
identyfikatora jako „ta nazwa odnosi się do właściwości”, a później zmiana tego na „ten wiersz kodu uzyskuje dostęp do tej właściwości w określony sposób”.
Aby rozwiązać ten problem, możesz potrzebować dwóch zestawów wyliczeń; jedno wyliczenie oznacza, co to jest nazwa (identyfikator); inne wyliczenie oznacza, co kod próbuje zrobić (np. deklarując treść czegoś; próbując użyć czegoś w określony sposób).
Zastanów się, czy zamiast tego można odczytać informacje pomocnicze o każdej wartości wyliczenia z tablicy.
Ta sugestia wyklucza się wzajemnie z innymi sugestiami, ponieważ wymaga zamiany potęg dwóch wartości z powrotem na małe nieujemne wartości całkowite.
public enum DeclarationType
{
Procedure = 8,
Function = 9,
Property = 10,
PropertyGet = 11,
PropertyLet = 12,
PropertySet = 13,
LibraryFunction = 23,
LibraryProcedure = 24,
}
static readonly bool[] DeclarationTypeIsMember = new bool[32]
{
?, ?, ?, ?, ?, ?, ?, ?, // bit[0] ... bit[7]
true, true, true, true, true, true, ?, ?, // bit[8] ... bit[15]
?, ?, ?, ?, ?, ?, ?, true, // bit[16] ... bit[23]
true, ... // bit[24] ...
}
static bool IsMember(DeclarationType dt)
{
int intValue = (int)dt;
return (intValue < 0 || intValue >= 32) ? false : DeclarationTypeIsMember[intValue];
// you can also throw an exception if the enum is outside range.
}
// likewise for IsFunction(dt), IsProcedure(dt), IsProperty(dt), ...
Utrzymanie będzie problematyczne.
Sprawdź, czy mapowanie jeden na jeden między typami C # (klasy w hierarchii dziedziczenia) i wartościami wyliczania.
(Alternatywnie możesz dostosować wartości wyliczeniowe, aby zapewnić mapowanie typu jeden do jednego z typami).
W języku C # wiele bibliotek nadużywa Type object.GetType()
dobrej i złej metody.
Gdziekolwiek przechowujesz wyliczenie jako wartość, możesz zadać sobie pytanie, czy Type
zamiast tego możesz zapisać wartość jako wartość.
Aby użyć tej sztuczki, możesz zainicjować dwie tabele skrótów tylko do odczytu, a mianowicie:
// For disambiguation, I'll assume that the actual
// (behavior-implementing) classes are under the
// "Lang" namespace.
static readonly Dictionary<Type, DeclarationType> TypeToDeclEnum = ...
{
{ typeof(Lang.Procedure), DeclarationType.Procedure },
{ typeof(Lang.Function), DeclarationType.Function },
{ typeof(Lang.Property), DeclarationType.Property },
...
};
static readonly Dictionary<DeclarationType, Type> DeclEnumToType = ...
{
// same as the first dictionary;
// just swap the key and the value
...
};
Ostateczne potwierdzenie dla osób sugerujących klasy i hierarchię dziedziczenia ...
Gdy zobaczysz, że wyliczenia są przybliżeniem do hierarchii dziedziczenia , obowiązuje następująca rada:
- Najpierw zaprojektuj (lub popraw) hierarchię dziedziczenia,
- Następnie wróć i zaprojektuj swoje wyliczenia, aby zbliżyć się do hierarchii dziedziczenia.
DeclarationType
. Jeśli chcę ustalić, czyx
podtyp jest, czy niey
, prawdopodobnie będę chciał napisać to jakox.IsSubtypeOf(y)
, a nie jakox && y == y
.