Wann ist die Typprüfung in Ordnung?

53

Angenommen, eine Sprache mit einer gewissen inhärenten Typensicherheit (z. B. kein JavaScript):

Ausgehend von einer Methode, die a akzeptiert SuperType, wissen wir, dass in den meisten Fällen die Versuchung besteht, Typprüfungen durchzuführen, um eine Aktion auszuwählen:

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

Wir sollten normalerweise, wenn nicht immer, eine einzelne, überschreibbare Methode auf dem erstellen SuperTypeund dies tun:

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

... wobei jeder Subtyp eine eigene doSomething()Implementierung erhält . Der Rest unserer Anwendung kann dann in geeigneter Weise unwissend sein, ob eine gegebene SuperTypewirklich eine SubTypeAoder eine ist SubTypeB.

Wunderbar

Wir erhalten jedoch immer noch is aähnliche Operationen in den meisten, wenn nicht allen typsicheren Sprachen. Dies deutet darauf hin, dass möglicherweise eine explizite Typprüfung erforderlich ist.

In welchen Situationen sollten oder müssen wir explizite Typprüfungen durchführen?

Vergib mir meine geistige Abwesenheit oder meinen Mangel an Kreativität. Ich weiß, dass ich es schon einmal getan habe. Aber es ist ehrlich gesagt so lange her, dass ich mich nicht erinnern kann, ob das, was ich getan habe, gut war! Und in den letzten Speicher, ich glaube nicht , dass ich je gesehen habe Notwendigkeit Arten außerhalb meines Cowboy JavaScript zu testen.

Svidgen
quelle
1
Lesen Sie mehr über Typinferenz und Typsysteme
Basile Starynkevitch
4
Es sei darauf hingewiesen, dass weder Java noch C # Generika in ihrer ersten Version hatten. Sie mussten zu und von Object umwandeln, um Container zu verwenden.
Doval
6
"Typüberprüfung" bezieht sich fast immer auf die Überprüfung, ob der Code die statischen Typisierungsregeln der Sprache einhält. Was Sie bedeuten , ist in der Regel Typ namens Test .
3
Aus diesem Grund schreibe ich gerne C ++ und lasse RTTI ausgeschaltet. Wenn man Objekttypen zur Laufzeit buchstäblich nicht testen kann, wird der Entwickler gezwungen, sich hinsichtlich der hier gestellten Frage an ein gutes OO-Design zu halten.

Antworten:

48

"Niemals" lautet die kanonische Antwort auf "Wann ist die Typprüfung in Ordnung?" Es gibt keine Möglichkeit, dies zu beweisen oder zu widerlegen. Es ist Teil eines Glaubenssystems darüber, was "gutes Design" oder "gutes objektorientiertes Design" ausmacht. Es ist auch hokum.

Um sicherzugehen, wenn Sie eine integrierte Menge von Klassen und mehr als eine oder zwei Funktionen haben, die diese Art der direkten Typprüfung benötigen, tun Sie es wahrscheinlich falsch. Was Sie wirklich brauchen, ist eine Methode, die in SuperTypeund in ihren Untertypen unterschiedlich implementiert ist . Dies ist ein wesentlicher Bestandteil der objektorientierten Programmierung, und die gesamten Grundklassen und Vererbungen existieren.

In diesem Fall ist ein expliziter Typentest falsch, nicht weil der Typentest von Natur aus falsch ist, sondern weil die Sprache bereits eine saubere, erweiterbare und idiomatische Methode zur Typunterscheidung bietet und Sie sie nicht verwendet haben. Stattdessen sind Sie auf eine primitive, fragile, nicht erweiterbare Sprache zurückgefallen.

Lösung: Verwenden Sie die Redewendung. Fügen Sie, wie Sie vorgeschlagen haben, jeder Klasse eine Methode hinzu, und lassen Sie dann die Standardvererbungs- und Methodenauswahlalgorithmen bestimmen, welcher Fall zutrifft. Wenn Sie die Basistypen nicht ändern können, fügen Sie dort Ihre Methode hinzu.

Soviel zur konventionellen Weisheit und zu einigen Antworten. Einige Fälle, in denen explizite Typprüfungen sinnvoll sind:

  1. Es ist ein Unikat. Wenn Sie viel Typunterscheidung zu tun hatten, könnten Sie die Typen oder Unterklassen erweitern. Aber du nicht. Sie haben nur ein oder zwei Stellen, an denen Sie explizite Tests benötigen. Es lohnt sich also nicht, die Klassenhierarchie noch einmal durchzuarbeiten, um die Funktionen als Methoden hinzuzufügen. Oder es ist den praktischen Aufwand nicht wert, die Art der Allgemeinheit, Tests, Entwurfsprüfungen, Dokumentation oder andere Attribute der Basisklassen für eine derart einfache, eingeschränkte Verwendung hinzuzufügen. In diesem Fall ist das Hinzufügen einer Funktion zum direkten Testen sinnvoll.

  2. Sie können die Klassen nicht anpassen. Sie denken über Unterklassen nach - aber Sie können nicht. Beispielsweise sind viele Klassen in Java vorgesehen final. Sie versuchen, ein einzuspielen, public class ExtendedSubTypeA extends SubTypeA {...} und der Compiler teilt Ihnen mit, dass das, was Sie tun, nicht möglich ist. Entschuldigung, Anmut und Raffinesse des objektorientierten Modells! Jemand hat entschieden, dass Sie ihre Typen nicht erweitern können! Leider gibt es viele Standardbibliotheken final, und das Erstellen von Klassen finalist eine gängige Gestaltungshilfe. Ein Funktionsabschluss ist das, was Ihnen noch zur Verfügung steht.

    Übrigens ist dies nicht auf statisch typisierte Sprachen beschränkt. Dynamische Sprache Python verfügt über eine Reihe von Basisklassen, die unter den in C implementierten Bedingungen nicht wirklich geändert werden können. Dies umfasst wie Java die meisten Standardtypen.

  3. Ihr Code ist extern. Sie entwickeln mit Klassen und Objekten, die von einer Reihe von Datenbankservern, Middleware-Engines und anderen Codebasen stammen, die Sie nicht steuern oder anpassen können. Ihr Code ist nur ein geringer Konsument von Objekten, die an anderer Stelle generiert wurden. Selbst wenn Sie eine Unterklasse SuperTypeerstellen könnten , sind Sie nicht in der Lage, die Bibliotheken abzurufen, von denen Sie abhängig sind, um Objekte in Ihren Unterklassen zu generieren. Sie geben Ihnen Instanzen der Typen, die sie kennen, und nicht Ihre Varianten. Dies ist nicht immer der Fall ... manchmal sind sie auf Flexibilität ausgelegt und instanziieren dynamisch Instanzen von Klassen, die Sie ihnen zuweisen. Oder sie bieten einen Mechanismus zum Registrieren der Unterklassen, die ihre Fabriken erstellen sollen. XML-Parser scheinen besonders gut darin zu sein, solche Einstiegspunkte bereitzustellen. siehe zb oder lxml in Python . Die meisten Codebasen bieten jedoch keine solchen Erweiterungen an. Sie werden Ihnen die Klassen zurückgeben, mit denen sie gebaut wurden und über die Sie Bescheid wissen. Im Allgemeinen ist es nicht sinnvoll, deren Ergebnisse in Ihre benutzerdefinierten Ergebnisse zu übernehmen, nur damit Sie eine rein objektorientierte Typenauswahl verwenden können. Wenn Sie Typendiskriminierung betreiben, müssen Sie dies relativ grob tun. Ihr Typprüfcode sieht dann ganz passend aus.

  4. Generika für Arme / Mehrfachversand. Sie möchten eine Vielzahl unterschiedlicher Typen in Ihren Code aufnehmen und sind der Meinung, dass ein Array von sehr typspezifischen Methoden nicht sinnvoll ist. public void add(Object x)logisch erscheint, aber keine von Array addByte, addShort, addInt, addLong, addFloat, addDouble, addBoolean, addChar, und addStringVarianten (um nur einige zu nennen). Mit Funktionen oder Methoden, die einen hohen Supertyp aufweisen und dann von Typ zu Typ entscheiden, was zu tun ist, werden Sie beim jährlichen Booch-Liskov Design Symposium nicht mit dem Purity Award ausgezeichnet, sondern mit dem Durch die ungarische Benennung erhalten Sie eine einfachere API. In gewissem Sinne Ihr is-aoderis-instance-of Beim Testen werden generische oder Mehrfachversendungen in einem Sprachkontext simuliert, der dies nicht nativ unterstützt.

    Durch die integrierte Sprachunterstützung für Generika und das Eingeben von Enten wird die Notwendigkeit einer Typprüfung verringert, indem die Wahrscheinlichkeit erhöht wird, dass "etwas Anmutiges und Angemessenes getan wird". Die in Sprachen wie Julia und Go vorkommende Mehrfachauswahl für Versand und Schnittstellen ersetzt in ähnlicher Weise die direkte Typprüfung durch integrierte Mechanismen für die typbasierte Auswahl von "was zu tun ist". Aber nicht alle Sprachen unterstützen diese. Java zB ist in der Regel Single-Dispatch und seine Redewendungen sind nicht besonders einfach zu tippen.

    Aber selbst bei all diesen Funktionen zur Typunterscheidung - Vererbung, Generika, Entenschreiben und Mehrfachversand - ist es manchmal einfach bequem, eine einzige, konsolidierte Routine zu haben, die die Tatsache berücksichtigt, dass Sie etwas basierend auf dem Typ des Objekts tun klar und unmittelbar. Bei der Metaprogrammierung habe ich festgestellt, dass dies im Wesentlichen unvermeidbar ist. Ob das Zurückgreifen auf direkte Typanfragen "Pragmatismus in Aktion" oder "Dirty Coding" bedeutet, hängt von Ihrer Designphilosophie und -überzeugung ab.

Jonathan Eunice
quelle
Wenn eine Operation nicht für einen bestimmten Typ ausgeführt werden kann, sondern für zwei verschiedene Typen, in die sie implizit konvertierbar wäre, ist eine Überladung nur dann sinnvoll, wenn beide Konvertierungen zu identischen Ergebnissen führen würden. Andernfalls kommt es zu beschissenen Verhaltensweisen wie in .NET: (1.0).Equals(1.0f)Nachgeben von true [Argument wird zu double], (1.0f).Equals(1.0)Nachgeben von false [Argument wird zu object]; in Java, Math.round(123456789*1.0)ergibt 123456789, sondern Math.round(123456789*1.0)liefert 123456792 [Argument fördert , um floatstatt double].
Supercat
Dies ist das klassische Argument gegen automatische Typumwandlung / -eskalation. Schlechte und paradoxe Ergebnisse, zumindest in Randfällen. Ich bin damit einverstanden, bin mir aber nicht sicher, wie Sie mit meiner Antwort umgehen sollen.
Jonathan Eunice
Ich habe auf Ihren vierten Punkt geantwortet, der anscheinend eine Überlastung befürwortet, anstatt Methoden mit unterschiedlichen Namen und Typen zu verwenden.
Supercat
2
@supercat Tadle meine Sehschwäche, aber die beiden Ausdrücke Math.roundsehen bei mir identisch aus. Was ist der Unterschied?
Lily Chung
2
@IstvanChung: Hoppla ... Letzteres sollte Math.round(123456789)[ein Hinweis darauf, was passieren könnte, wenn jemand Math.round(thing.getPosition() * COUNTS_PER_MIL)einen nicht skalierten Positionswert zurückgibt und nicht merkt, dass getPositionein intoder zurückgegeben wird long.]
supercat
25

Die Hauptsituation, die ich jemals gebraucht habe, war beim Vergleichen von zwei Objekten, z. B. in einer equals(other)Methode, die je nach Typ unterschiedliche Algorithmen erfordern kann other. Sogar dann ist es ziemlich selten.

Die andere Situation, auf die ich es wieder sehr selten kommen lassen habe, ist nach der Deserialisierung oder dem Parsen, wo Sie es manchmal benötigen, um sicher auf einen spezifischeren Typ zu gießen.

Manchmal brauchen Sie auch nur einen Hack, um Code von Drittanbietern zu umgehen, den Sie nicht kontrollieren. Es ist eines der Dinge, die Sie nicht wirklich regelmäßig verwenden möchten, aber Sie sind froh, dass es da ist, wenn Sie es wirklich brauchen.

Karl Bielefeldt
quelle
1
Der Fall der Deserialisierung hat mich augenblicklich entzückt, weil ich mich fälschlicherweise daran erinnert habe, ihn dort verwendet zu haben. aber jetzt kann ich mir nicht vorstellen, wie ich hätte! Ich weiß, dass ich einige seltsame Typ-Lookups durchgeführt habe. aber ich bin mir nicht sicher , ob diese Art darstellt Prüfung . Vielleicht ist die Serialisierung eine engere Parallele: Sie müssen Objekte nach ihrem konkreten Typ abfragen.
Svidgen
1
Serialisierung ist normalerweise mit Polymorphismus möglich. Beim Deserialisieren tun Sie häufig etwas BaseClass base = deserialize(input), weil Sie den Typ noch nicht kennen, und if (base instanceof Derived) derived = (Derived)basespeichern ihn dann als den genau abgeleiteten Typ.
Karl Bielefeldt
1
Gleichheit macht Sinn. Nach meiner Erfahrung haben solche Methoden häufig die Form: „Wenn diese beiden Objekte denselben konkreten Typ haben, geben Sie zurück, ob alle Felder gleich sind. Andernfalls geben Sie false (oder noncomparable) zurück.
Jon Purdy
2
Insbesondere die Tatsache, dass Sie feststellen, dass Sie den Typ testen, ist ein guter Indikator dafür, dass polymorphe Gleichheitsvergleiche ein Nest von Vipern sind.
Steve Jessop
12

Der Standardfall (aber hoffentlich selten) sieht so aus: wenn in der folgenden Situation

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

die Funktionen DoSomethingAoder DoSomethingBkönnen nicht einfach als Mitgliedsfunktionen des Vererbungsbaums von SuperType/ SubTypeA/ implementiert werden SubTypeB. Zum Beispiel, wenn

  • Die Untertypen sind Teil einer Bibliothek, die Sie nicht ändern können
  • Wenn Sie den Code für DoSomethingXXXdiese Bibliothek hinzufügen , würde dies bedeuten, dass eine verbotene Abhängigkeit eingeführt wird.

Beachten Sie, dass es häufig Situationen gibt, in denen Sie dieses Problem umgehen können (z. B. durch Erstellen eines Wrappers oder Adapters für SubTypeAund SubTypeBoder durch den Versuch einer DoSomethingvollständigen Neuimplementierung in Bezug auf grundlegende Vorgänge von SuperType), aber manchmal sind diese Lösungen den Aufwand oder die Mühe nicht wert Dinge komplizierter und weniger erweiterbar als die explizite Typprüfung.

Ein Beispiel aus meiner gestrigen Arbeit: Ich hatte eine Situation, in der ich die Verarbeitung einer Liste von Objekten (vom Typ SuperTypemit genau zwei verschiedenen Untertypen, bei denen es äußerst unwahrscheinlich ist, dass es jemals mehr geben wird) parallelisieren wollte . Die unvergleichliche Version enthielt zwei Schleifen: eine Schleife für Objekte des Subtyps A mit Aufruf DoSomethingAund eine zweite Schleife für Objekte des Subtyps B mit Aufruf DoSomethingB.

Die Methoden "DoSomethingA" und "DoSomethingB" sind zeitintensive Berechnungen, bei denen Kontextinformationen verwendet werden, die im Bereich der Subtypen A und B nicht verfügbar sind (daher ist es nicht sinnvoll, sie als Elementfunktionen der Subtypen zu implementieren). Aus der Sicht der neuen "Parallelschleife" vereinfacht sich die Arbeit erheblich, da sie einheitlich behandelt wird. Daher habe ich eine Funktion implementiert, die der DoSomethingTovon oben ähnelt . Ein Blick auf die Implementierungen von "DoSomethingA" und "DoSomethingB" zeigt jedoch, dass sie intern sehr unterschiedlich funktionieren. Der Versuch, ein generisches "DoSomething" durch Erweiterung SuperTypemit vielen abstrakten Methoden zu implementieren , würde also nicht wirklich funktionieren oder bedeuten, die Dinge komplett zu überarbeiten.

Doc Brown
quelle
2
Kannst du ein kleines konkretes Beispiel hinzufügen, um mein nebliges Gehirn davon zu überzeugen, dass dieses Szenario nicht erfunden ist?
Svidgen
Um es klar, ich sage nicht , es ist gekünstelt. Ich vermute, ich bin heute einfach nur total beschlagen.
Svidgen
@svidgen: das ist alles andere als erfunden, eigentlich bin ich heute auf diese Situation gestoßen (obwohl ich dieses Beispiel hier nicht posten kann, weil es geschäftliche Interna enthält). Und ich stimme den anderen Antworten zu, dass die Verwendung des "Ist" -Operators eine Ausnahme sein sollte und nur in seltenen Fällen erfolgt.
Doc Brown
Ich denke, Ihre Bearbeitung, die ich übersehen habe, gibt ein gutes Beispiel. ... Gibt es Fälle , bei denen dies in Ordnung ist , auch wenn Sie keine Kontrolle SuperTypeund es ist Subklassen?
Svidgen
6
Hier ein konkretes Beispiel: Der Container auf oberster Ebene in einer JSON-Antwort von einem Webdienst kann entweder ein Wörterbuch oder ein Array sein. Normalerweise verfügen Sie über ein Tool, mit dem JSON in echte Objekte umgewandelt wird (z. B. NSJSONSerializationin Obj-C). Sie möchten jedoch nicht einfach darauf vertrauen, dass die Antwort den erwarteten Typ enthält. Überprüfen Sie sie daher, bevor Sie sie verwenden (z. B. if ([theResponse isKindOfClass:[NSArray class]])...). .
Caleb
5

Wie Onkel Bob es nennt:

When your compiler forgets about the type.

In einer seiner Clean Coder-Episoden gab er ein Beispiel für einen Funktionsaufruf, mit dem Employees zurückgegeben wird. Managerist ein Untertyp von Employee. Nehmen wir an, wir haben einen Anwendungsdienst, der die ManagerID eines Benutzers akzeptiert und ihn ins Büro ruft :) Die Funktion gibt getEmployeeById()einen Supertyp zurück Employee, aber ich möchte überprüfen, ob in diesem Anwendungsfall ein Manager zurückgegeben wird.

Zum Beispiel:

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

Hier überprüfe ich, ob es sich bei dem von der Abfrage zurückgegebenen Mitarbeiter tatsächlich um einen Manager handelt (dh ich erwarte, dass es sich um einen Manager handelt und ob dies ansonsten schnell fehlschlägt).

Nicht das beste Beispiel, aber es ist doch Onkel Bob.

Aktualisieren

Ich habe das Beispiel aktualisiert, so weit ich mich erinnern kann.

Songo
quelle
1
Warum nicht Manager‚s Umsetzung summon()nur die Ausnahme in diesem Beispiel werfen?
Svidgen
@svidgen vielleicht CEOkann der Managers beschwören .
user253751
@svidgen, Es wäre dann nicht so klar, dass employeeRepository.getEmployeeById (empId) voraussichtlich einen Manager zurückgibt
Ian
@ Ian Ich sehe das nicht als Problem. Wenn der aufrufende Code nach einem fragt Employee, sollte es nur wichtig sein, dass er etwas erhält, das sich wie ein verhält Employee. Wenn verschiedene Unterklassen Employeeunterschiedliche Berechtigungen, Verantwortlichkeiten usw. haben, was macht die Typprüfung zu einer besseren Option als ein echtes Berechtigungssystem?
Svidgen
3

Wann ist die Typprüfung in Ordnung?

Noch nie.

  1. Wenn Sie das mit diesem Typ verknüpfte Verhalten in einer anderen Funktion haben, verstoßen Sie gegen das Open Closed-Prinzip , da Sie das vorhandene Verhalten des Typs ändern können, indem Sie die isKlausel oder (in einigen Sprachen oder abhängig von der) ändern Szenario), da Sie den Typ nicht erweitern können, ohne die Interna der Funktion zu ändern, die die isPrüfung durchführt.
  2. Noch wichtiger ist, dass isSchecks ein starkes Zeichen dafür sind, dass Sie gegen das Liskov-Substitutionsprinzip verstoßen . Alles, was damit funktioniert, SuperTypesollte völlig unbekannt sein, welche Subtypen es möglicherweise gibt.
  3. Sie verknüpfen implizit ein bestimmtes Verhalten mit dem Namen des Typs. Dies erschwert die Pflege Ihres Codes, da diese impliziten Verträge über den gesamten Code verteilt sind und nicht wie die tatsächlichen Klassenmitglieder durchgesetzt werden können.

, Die alle gesagt, iskann überprüft werden weniger schlecht als andere Alternativen. Das Einordnen aller gängigen Funktionen in eine Basisklasse ist umständlich und führt häufig zu schlimmeren Problemen. Die Verwendung einer einzelnen Klasse mit einem Flag oder einer Aufzählung für den Typ der Instanz ist schlimmer als schrecklich, da Sie die Umgehung des Typsystems jetzt auf alle Konsumenten ausdehnen.

Kurz gesagt, Sie sollten Typüberprüfungen immer als starken Codegeruch betrachten. Aber wie bei allen Richtlinien wird es Zeiten geben, in denen Sie sich entscheiden müssen, welche Richtlinienverletzung am wenigsten anstößig ist.

Telastyn
quelle
3
Es gibt eine kleine Einschränkung: Implementierung algebraischer Datentypen in Sprachen, die diese nicht unterstützen. Die Verwendung von Vererbung und Typchecking ist dort jedoch nur ein hackiges Implementierungsdetail. Die Absicht ist nicht, Untertypen einzuführen, sondern Werte zu kategorisieren. Ich rufe es nur auf, weil ADTs nützlich sind und niemals ein sehr starkes Qualifikationsmerkmal darstellen, aber ansonsten stimme ich voll und ganz zu. instanceofleckt Implementierungsdetails und unterbricht die Abstraktion.
Doval
17
"Niemals" ist ein Wort, das ich in einem solchen Kontext nicht wirklich mag, besonders wenn es im Widerspruch zu dem steht, was Sie unten schreiben.
Doc Brown
10
Wenn Sie mit "nie" tatsächlich "manchmal" meinen, dann haben Sie recht.
Caleb
2
OK bedeutet zulässig, akzeptabel usw., aber nicht unbedingt ideal oder optimal. Im Gegensatz zu " nicht OK" : Wenn etwas nicht in Ordnung ist, sollten Sie es überhaupt nicht tun. Wie Sie angeben, kann die Notwendigkeit, den Typ von etwas zu überprüfen, ein Hinweis auf tiefere Probleme im Code sein, aber es gibt Zeiten, in denen dies die zweckmäßigste und am wenigsten schlechte Option ist, und in solchen Situationen ist es offensichtlich in Ordnung , die Tools bei Ihnen zu verwenden Verfügung. (Wenn es einfach wäre, sie in allen Situationen zu vermeiden, wären sie wahrscheinlich gar nicht erst da.) Die Frage läuft darauf hinaus, diese Situationen zu identifizieren, und hilft nie .
Caleb
2
@supercat: Eine Methode sollte den geringstmöglichen spezifischen Typ erfordern, der ihre Anforderungen erfüllt . IEnumerable<T>verspricht nicht, dass ein "letztes" Element existiert. Wenn Ihre Methode ein solches Element benötigt, sollte ein Typ erforderlich sein, der die Existenz eines solchen Elements garantiert. Und die Subtypen dieses Typs können dann effiziente Implementierungen der "Last" -Methode liefern.
CHAO
2

Wenn Sie eine große Codebasis (über 100 KByte Codezeilen) haben und in der Nähe des Versands sind oder in einer Zweigstelle arbeiten, die später zusammengeführt werden muss, ist die Änderung einer großen Menge an Code mit hohen Kosten und Risiken verbunden.

Sie haben dann manchmal die Möglichkeit, einen großen Refraktor des Systems oder eine einfache, lokalisierte „Typprüfung“ durchzuführen. Dadurch entsteht eine technische Verschuldung, die schnellstmöglich zurückgezahlt werden sollte, aber häufig nicht.

(Es ist unmöglich, ein Beispiel zu finden, da jeder Code, der klein genug ist, um als Beispiel verwendet zu werden, auch klein genug ist, damit das bessere Design klar sichtbar ist.)

Oder mit anderen Worten, wenn das Ziel darin besteht, Ihren Lohn zu zahlen, anstatt für die Sauberkeit Ihres Designs „up votes“ zu erhalten.


Der andere häufige Fall ist der UI-Code, wenn Sie zum Beispiel für einige Arten von Mitarbeitern eine andere UI anzeigen, aber natürlich nicht möchten, dass die UI-Konzepte in alle Domänenklassen übergehen.

Mit "Typentest" können Sie entscheiden, welche Version der Benutzeroberfläche angezeigt werden soll, oder Sie können eine ausgefallene Nachschlagetabelle verwenden, die von "Domänenklassen" in "Benutzeroberflächenklassen" konvertiert. Die Nachschlagetabelle ist nur eine Möglichkeit, die „Typprüfung“ an einem Ort zu verbergen.

(Der Datenbankaktualisierungscode kann dieselben Probleme wie der Benutzeroberflächencode haben, Sie verfügen jedoch in der Regel nur über einen Satz von Datenbankaktualisierungscode. Sie können jedoch viele verschiedene Bildschirme verwenden, die an den angezeigten Objekttyp angepasst werden müssen.)

Ian
quelle
Das Besuchermuster ist häufig ein guter Weg, um Ihren UI-Fall zu lösen.
Ian Goldby
@IanGoldby, manchmal ist man sich einig, dass es sein kann, aber Sie machen immer noch "Typprüfung", nur ein bisschen versteckt.
Ian
Versteckt in dem Sinne, dass es versteckt ist, wenn Sie eine reguläre virtuelle Methode aufrufen? Oder meinst du etwas anderes? Das von mir verwendete Besuchermuster enthält keine typabhängigen bedingten Anweisungen. Es wird alles von der Sprache erledigt.
Ian Goldby
@ IanGoldby, ich meinte versteckt in dem Sinne, dass es den WPF- oder WinForms-Code schwerer verständlich machen kann. Ich erwarte, dass es für einige Web-Base-Benutzeroberflächen sehr gut funktionieren würde.
Ian
2

Die Implementierung von LINQ verwendet eine Vielzahl von Typprüfungen für mögliche Leistungsoptimierungen und dann einen Fallback für IEnumerable.

Das offensichtlichste Beispiel ist wahrscheinlich die ElementAt-Methode (winziger Auszug aus dem .NET 4.5-Quellcode):

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

Es gibt jedoch viele Stellen in der Enumerable-Klasse, an denen ein ähnliches Muster verwendet wird.

Daher ist die Optimierung der Leistung für einen häufig verwendeten Subtyp möglicherweise eine gültige Verwendung. Ich bin mir nicht sicher, wie dies besser hätte gestaltet werden können.

Menno van den Heuvel
quelle
Es hätte besser entworfen werden können, indem ein praktisches Mittel bereitgestellt wurde, mit dem Schnittstellen Standardimplementierungen bereitstellen können, und dann IEnumerable<T>viele Methoden wie die in List<T>zusammen mit einer FeaturesEigenschaft enthalten sind, die angibt, von welchen Methoden erwartet werden kann, dass sie gut, langsam oder überhaupt nicht funktionieren. sowie verschiedene Annahmen, die ein Verbraucher in Bezug auf die Sammlung sicher treffen kann (z. B. wird garantiert, dass sich die Größe und / oder der vorhandene Inhalt der Sammlung nicht ändern (ein Typ könnte dies unterstützen Addund dennoch garantieren, dass der vorhandene Inhalt unveränderlich ist)).
Supercat
Außer in Szenarien, in denen ein Typ möglicherweise zwei völlig unterschiedliche Dinge zu unterschiedlichen Zeiten enthalten muss und sich die Anforderungen eindeutig ausschließen (und die Verwendung separater Felder daher überflüssig wäre), betrachte ich die Notwendigkeit eines Try-Castings im Allgemeinen als Zeichen Mitglieder, die Teil einer Basisschnittstelle sein sollten, waren es nicht. Das soll nicht heißen, dass Code, der eine Schnittstelle verwendet, die einige Member enthalten sollte, aber TryCasting nicht verwenden sollte, um die Auslassung der Basisschnittstelle zu umgehen, aber diese schreibenden Basisschnittstellen sollten die Notwendigkeit von TryCasting für Clients minimieren.
Supercat
1

Es gibt ein Beispiel, das in Spieleentwicklungen häufig vorkommt, insbesondere bei der Kollisionserkennung, und das ohne die Verwendung einer Art von Typprüfung schwer zu handhaben ist.

Angenommen, alle Spielobjekte stammen von einer gemeinsamen Basisklasse GameObject. Jedes Objekt hat eine starre Körper Kollisionsform , CollisionShapedie eine gemeinsame Schnittstelle zur Verfügung stellen kann (query Position zu sagen, Orientierung, etc.) , aber die eigentlichen Kollisionsformen werden alle konkrete Subklassen sein , wie beispielsweise Sphere, Box, ConvexHullusw. Speicher von Informationen , die spezifisch für diese Art von geometrischem Objekt (siehe hier für ein reales Beispiel)

Um nun auf Kollisionen zu testen, muss ich eine Funktion für jedes Paar von Kollisionsformtypen schreiben:

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

Diese enthalten die spezifische Mathematik, die zum Durchführen einer Schnittmenge dieser beiden geometrischen Typen erforderlich ist.

Bei jedem Tick meiner Spielrunde muss ich zwei Objekte auf Kollisionen prüfen. Ich habe aber nur Zugriff auf GameObjects und die entsprechenden CollisionShapes. Natürlich muss ich konkrete Typen kennen, um zu wissen, welche Kollisionserkennungsfunktion aufgerufen werden soll. Hier kann nicht einmal der doppelte Versand helfen (was logischerweise nichts anderes ist, als den Typ zu überprüfen) *.

In der Praxis verlassen sich die von mir gesehenen Physik-Motoren (Bullet und Havok) in dieser Situation auf Typprüfungen der einen oder anderen Form.

Ich sage nicht, dass dies notwendigerweise eine gute Lösung ist, es ist nur so, dass es das Beste aus einer kleinen Anzahl möglicher Lösungen für dieses Problem ist

* Technisch es ist möglich , Doppel - Ausgabe in einer schrecklichen und komplizierten Art und Weise zu verwenden , die N (N + 1) / 2 Kombinationen erfordern würden (wobei N die Anzahl von Formtypen Sie haben) zur Verfügung und würde nur verschleiern , was Sie wirklich tun was Indem ich gleichzeitig die Typen der beiden Formen herausfinde, halte ich dies nicht für eine realistische Lösung.

Martin
quelle
1

Manchmal möchten Sie nicht allen Klassen eine gemeinsame Methode hinzufügen, da es nicht in ihrer Verantwortung liegt, diese bestimmte Aufgabe auszuführen.

Zum Beispiel möchten Sie einige Objekte zeichnen, aber den Zeichnungscode nicht direkt hinzufügen (was sinnvoll ist). In Sprachen, die keinen Mehrfachversand unterstützen, erhalten Sie möglicherweise den folgenden Code:

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

Dies ist problematisch, wenn dieser Code an mehreren Stellen angezeigt wird und Sie ihn überall ändern müssen, wenn Sie einen neuen Entitätstyp hinzufügen. Wenn dies der Fall ist, kann dies durch die Verwendung des Besuchermusters vermieden werden. Manchmal ist es jedoch besser, die Dinge einfach zu halten und nicht zu überdenken. In diesen Situationen ist die Typprüfung in Ordnung.

Honza Brabec
quelle
0

Ich benutze nur die Kombination mit Reflektion. Aber selbst dann ist die dynamische Prüfung meistens nicht fest auf eine bestimmte Klasse festgelegt (oder nur fest auf spezielle Klassen wie Stringoder festgelegt List).

Mit dynamischer Überprüfung meine ich:

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

    }
}

und nicht fest codiert

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

    }
}
m3th0dman
quelle
0

Typprüfung und Typguss sind zwei sehr eng verwandte Konzepte. So eng verwandt, dass ich zuversichtlich bin, dass Sie niemals eine Typprüfung durchführen sollten, es sei denn, Sie möchten das Objekt basierend auf dem Ergebnis tippen.

Wenn denken Sie über ideale Objektorientiertes Design, Typprüfung (und Gießen) sollte nie passieren. Aber hoffentlich haben Sie inzwischen herausgefunden, dass objektorientierte Programmierung nicht ideal ist. Manchmal, insbesondere bei Code auf niedrigerer Ebene, kann der Code nicht dem Ideal entsprechen. Dies ist bei ArrayLists in Java der Fall. Da sie zur Laufzeit nicht wissen, welche Klasse im Array gespeichert ist, erstellen sie Object[]Arrays und wandeln sie statisch in den richtigen Typ um.

Es wurde darauf hingewiesen, dass ein allgemeines Bedürfnis nach Typentests (und Typguss) von der EqualsMethode herrührt, die in den meisten Sprachen eine Ebene annehmen soll Object. In der Implementierung sollten einige detaillierte Überprüfungen vorgenommen werden, um festzustellen, ob die beiden Objekte vom selben Typ sind, und um zu testen, um welchen Typ es sich handelt.

Auch die Typprüfung spiegelt sich häufig wider. Oft haben Sie Methoden, die zurückgeben, Object[]oder ein anderes generisches Array, und Sie möchten alle FooObjekte aus irgendeinem Grund herausziehen . Dies ist eine absolut legitime Verwendung der Typprüfung und des Gießens.

Im Allgemeinen sind Typentests schlecht, wenn der Code unnötigerweise an die Art und Weise gekoppelt wird, in der eine bestimmte Implementierung geschrieben wurde. Dies kann leicht dazu führen, dass für jeden Typ oder jede Kombination von Typen ein spezifischer Test erforderlich ist, z. B. wenn Sie den Schnittpunkt von Linien, Rechtecken und Kreisen ermitteln möchten und die Schnittpunktfunktion für jede Kombination einen anderen Algorithmus hat. Ihr Ziel ist es, Details, die für eine Art von Objekt spezifisch sind, an derselben Stelle wie dieses Objekt zu platzieren, da dies die Pflege und Erweiterung Ihres Codes erleichtert.

Meustrus
quelle
1
ArrayListsIch kenne die zur Laufzeit gespeicherte Klasse nicht, da Java keine Generics hatte und Oracle sich bei der endgültigen Einführung für die Abwärtskompatibilität mit generics-losem Code entschieden hat. equalshat das gleiche Problem, und es ist sowieso eine fragwürdige Designentscheidung; Gleichstellungsvergleiche sind nicht für jeden Typ sinnvoll.
Doval
1
Technisch haben Java Sammlungen nicht dessen Inhalt allem werfen. Der Compiler fügt bei jedem Zugriff Typecasts ein: zBString x = (String) myListOfStrings.get(0)
Das letzte Mal, als ich mir die Java-Quelle (möglicherweise 1.6 oder 1.5) ansah, gab es eine explizite Besetzung in der ArrayList-Quelle. Es wird aus gutem Grund eine (unterdrückte) Compiler-Warnung generiert, die jedoch trotzdem zulässig ist. Ich nehme an, Sie könnten sagen, dass es aufgrund der Art und Weise, wie Generika implementiert werden, immer eine war, Objectbis auf sie trotzdem zugegriffen wurde. Generics in Java bieten nur implizites Casting, das durch Compiler-Regeln abgesichert ist.
meustrus
0

Dies ist in einem Fall akzeptabel, in dem Sie eine Entscheidung treffen müssen, die zwei Typen umfasst , und diese Entscheidung in einem Objekt außerhalb dieser Typenhierarchie gekapselt ist. Angenommen, Sie planen, welches Objekt als Nächstes in einer Liste von Objekten verarbeitet wird, die auf die Verarbeitung warten:

abstract class Vehicle
{
    abstract void Process();
}

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

Nehmen wir nun an, unsere Geschäftslogik lautet wörtlich "alle Autos haben Vorrang vor Booten und Lastwagen". Wenn Sie Priorityder Klasse eine Eigenschaft hinzufügen , können Sie diese Geschäftslogik nicht sauber ausdrücken, da dies zu folgendem Ergebnis führt:

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; } } ... }

Das Problem ist, dass Sie zum Verständnis der Prioritätsreihenfolge alle Unterklassen betrachten müssen, oder mit anderen Worten, Sie haben eine Kopplung zu den Unterklassen hinzugefügt.

Sie sollten die Prioritäten natürlich in Konstanten umwandeln und sie für sich in eine Klasse einteilen, um die Planung der Geschäftslogik zusammenzuhalten:

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

In Wirklichkeit ist der Planungsalgorithmus jedoch etwas, das sich in der Zukunft ändern und letztendlich von mehr als nur dem Typ abhängen kann. Zum Beispiel könnte es heißen: "Lastwagen mit einem Gewicht von über 5000 kg haben Vorrang vor allen anderen Fahrzeugen." Aus diesem Grund gehört der Planungsalgorithmus zu einer eigenen Klasse, und es ist eine gute Idee , den Typ zu untersuchen, um zu bestimmen, welcher zuerst ausgeführt werden soll:

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

Dies ist die einfachste Art, die Geschäftslogik zu implementieren, und dennoch die flexibelste für zukünftige Änderungen.

Scott Whitlock
quelle
Ich stimme Ihrem Beispiel nicht zu, würde aber dem Grundsatz zustimmen, eine private Referenz zu haben, die unterschiedliche Typen mit unterschiedlichen Bedeutungen enthalten kann. Als besseres Beispiel würde ich ein Feld vorschlagen, das entweder nulla Stringoder a enthalten kann String[]. Wenn 99% der Objekte genau eine Zeichenfolge benötigen, kann das Einkapseln jeder Zeichenfolge in eine separat erstellte Zeichenfolge einen String[]erheblichen Speicheraufwand verursachen. Die Behandlung des Einzelzeichenfolgenfalls unter Verwendung eines direkten Verweises auf a Stringerfordert mehr Code, spart jedoch Speicherplatz und beschleunigt möglicherweise die Arbeit.
Supercat
0

Die Typprüfung ist ein Werkzeug, das mit Bedacht eingesetzt wird und ein mächtiger Verbündeter sein kann. Verwenden Sie es schlecht und Ihr Code wird anfangen zu riechen.

In unserer Software haben wir als Antwort auf Anfragen Nachrichten über das Netzwerk erhalten. Alle deserialisierten Nachrichten hatten eine gemeinsame Basisklasse Message.

Die Klassen selbst waren sehr einfach, nur die Nutzdaten als typisierte C # -Eigenschaften und Routinen zum Zuordnen und Zuordnen (Tatsächlich habe ich die meisten Klassen mit t4-Vorlagen aus der XML-Beschreibung des Nachrichtenformats generiert).

Code wäre so etwas wie:

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

Zugegeben, man könnte argumentieren, dass die Nachrichtenarchitektur besser entworfen werden könnte, aber sie wurde vor langer Zeit entworfen und nicht für C #, also ist es das, was es ist. Hier hat die Typprüfung ein echtes Problem für uns nicht allzu schäbig gelöst.

Bemerkenswert ist, dass C # 7.0 eine Mustererkennung erhält (was in vielerlei Hinsicht ein Typentest für Steroide ist), es kann nicht alles schlecht sein ...

Isak Savo
quelle
0

Nehmen Sie einen generischen JSON-Parser. Das Ergebnis einer erfolgreichen Analyse ist ein Array, ein Wörterbuch, eine Zeichenfolge, eine Zahl, ein Boolescher Wert oder ein Nullwert. Es kann einer von denen sein. Und die Elemente eines Arrays oder die Werte in einem Wörterbuch können wieder alle diese Typen sein. Da die Daten von außerhalb Ihres Programms bereitgestellt werden, müssen Sie jedes Ergebnis akzeptieren (das heißt, Sie müssen es akzeptieren, ohne abstürzen zu müssen; Sie können ein Ergebnis ablehnen, das nicht Ihren Erwartungen entspricht).

gnasher729
quelle
Nun, einige JSON-Deserialisierer werden versuchen, einen Objektbaum zu instanziieren, wenn Ihre Struktur entweder Typinformationen für "Inferenzfelder" enthält. Aber ja. Ich denke, dies ist die Richtung, in die die Antwort von Karl B geleitet wurde.
svidgen