Wie soll ich "unbekannte" und "fehlende" Werte in einer Variablen speichern und trotzdem den Unterschied zwischen "unbekannten" und "fehlenden" beibehalten?

57

Betrachten Sie dies als eine "akademische" Frage. Ich habe darüber nachgedacht, von Zeit zu Zeit Nullen zu vermeiden, und dies ist ein Beispiel, bei dem ich keine zufriedenstellende Lösung finden kann.


Angenommen, ich speichere Messungen, bei denen gelegentlich bekannt ist, dass die Messung unmöglich ist (oder fehlt). Ich möchte diesen "leeren" Wert unter Vermeidung von NULL in einer Variablen speichern. In anderen Fällen ist der Wert möglicherweise unbekannt. Wenn also die Messungen für einen bestimmten Zeitraum vorliegen, kann eine Abfrage nach einer Messung innerhalb dieses Zeitraums drei Arten von Antworten zurückgeben:

  • Die tatsächliche Messung zu diesem Zeitpunkt (z. B. ein beliebiger numerischer Wert einschließlich 0)
  • Ein "fehlender" / "leerer" Wert (dh es wurde eine Messung durchgeführt, und der Wert ist zu diesem Zeitpunkt als leer bekannt ).
  • Ein unbekannter Wert (dh zu diesem Zeitpunkt wurde noch keine Messung durchgeführt. Es kann leer sein, es kann sich aber auch um einen anderen Wert handeln).

Wichtige Klarstellung:

Angenommen, Sie hatten eine Funktion, get_measurement()die "leer", "unbekannt" und einen Wert vom Typ "Ganzzahl" zurückgibt. Ein numerischer Wert impliziert, dass bestimmte Operationen für den Rückgabewert ausgeführt werden können (Multiplikation, Division, ...), aber die Verwendung solcher Operationen für NULL-Werte führt zum Absturz der Anwendung, wenn sie nicht abgefangen werden.

Ich möchte in der Lage sein, Code zu schreiben, zum Beispiel ohne NULL-Prüfungen (Pseudocode):

>>> value = get_measurement()  # returns `2`
>>> print(value * 2)
4

>>> value = get_measurement()  # returns `Empty()`
>>> print(value * 2)
Empty()

>>> value = get_measurement()  # returns `Unknown()`
>>> print(value * 2)
Unknown()

Beachten Sie, dass keine der printAnweisungen Ausnahmen verursachte (da keine NULL-Werte verwendet wurden). Die leeren und unbekannten Werte würden sich also nach Bedarf ausbreiten, und die Überprüfung, ob ein Wert tatsächlich "unbekannt" oder "leer" ist, könnte verzögert werden, bis er wirklich notwendig ist (wie das Speichern / Serialisieren des Wertes irgendwo).


Randbemerkung: Der Grund, warum ich NULL vermeiden möchte, ist in erster Linie ein Rätsel. Wenn ich etwas erledigen möchte, bin ich nicht gegen die Verwendung von NULL-Werten, aber ich habe festgestellt, dass das Vermeiden von NULL-Werten den Code in einigen Fällen viel robuster machen kann.

Exhuma
quelle
19
Warum möchten Sie zwischen "Messung durchgeführt, aber leerer Wert" und "keine Messung" unterscheiden? Was bedeutet eigentlich "Messung durchgeführt, aber leerer Wert"? Hat der Sensor keinen gültigen Wert erzeugt? Wie unterscheidet sich das in diesem Fall von "unbekannt"? Sie werden nicht in der Lage sein, in die Vergangenheit zu reisen und den richtigen Wert zu erhalten.
DaveG
3
@DaveG Es wird angenommen, dass die Anzahl der CPUs in einem Server abgerufen wird. Wenn der Server ausgeschaltet ist oder verschrottet wurde, existiert dieser Wert einfach nicht. Es wird eine Messung sein, die keinen Sinn ergibt ("fehlt" / "leer" sind möglicherweise nicht die besten Begriffe). Aber der Wert ist "bekannt" als unsinnig. Wenn der Server vorhanden ist, der Vorgang zum Abrufen des Werts jedoch abstürzt, ist die Messung gültig, schlägt jedoch fehl und führt zu einem "unbekannten" Wert.
Exhuma
2
@exhuma Ich würde es dann als "nicht zutreffend" bezeichnen.
Vincent
6
Welche Art von Messung machen Sie aus Neugier, wo "leer" nicht einfach gleich der Null ist, egal auf welcher Skala? "Unbekannt" / "nicht vorhanden" Ich sehe es als nützlich an, wenn ein Sensor nicht angeschlossen ist oder wenn die Rohdaten des Sensors aus dem einen oder anderen Grund Müll sind, aber "leer" in jedem Fall, den ich mir vorstellen kann, konsistenter sein kann dargestellt durch 0, []oder {}(der Skalar 0, die leere Liste bzw. die leere Karte). Auch die „fehlender“ / „unbekannt“ Wert ist im Grunde genau das, was nullfür - es bedeutet , dass es könnte dort ein Objekt sein, aber es ist nicht.
Nic Hartley
7
Unabhängig davon, welche Lösung Sie dafür verwenden, müssen Sie sich fragen, ob ähnliche Probleme vorliegen wie bei denjenigen, die Sie veranlasst haben, NULL zu beseitigen.
Ray

Antworten:

85

Zumindest bei funktionalen Sprachen ist es üblich, eine diskriminierte Union zu verwenden. Dies ist dann ein Wert, der eines von einem gültigen int ist, ein Wert, der "fehlt" oder ein Wert, der "unbekannt" anzeigt. In F # könnte es ungefähr so ​​aussehen:

type Measurement =
    | Reading of value : int
    | Missing
    | Unknown of value : RawData

Ein MeasurementWert ist dann a Reading, mit einem int-Wert oder a Missingoder Unknownmit den Rohdaten als value(falls erforderlich).

Wenn Sie jedoch keine Sprache verwenden, die diskriminierte Gewerkschaften oder deren Äquivalente unterstützt, ist dieses Muster für Sie wahrscheinlich nicht von großem Nutzen. Dort können Sie beispielsweise eine Klasse mit einem Aufzählungsfeld verwenden, das angibt, welche der drei Klassen die richtigen Daten enthält.

David Arno
quelle
7
Sie können Summentypen
jk.
11
"[In nicht-funktionalen Sprachen Sprachen] dieses Muster ist wahrscheinlich nicht von großem Nutzen für Sie" - Es ist ein ziemlich verbreitetes Muster in OOP. GOF hat eine Variation dieses Musters und Sprachen wie C ++ bieten native Konstrukte, um es zu codieren.
Konrad Rudolph
14
@jk. Ja, sie zählen nicht (naja, ich denke schon; sie sind in diesem Szenario aufgrund mangelnder Sicherheit nur sehr schlecht). Ich meinte std::variant(und seine spirituellen Vorgänger).
Konrad Rudolph
2
@Ewan Nein, es heißt "Messung ist ein Datentyp, der entweder ... oder ... ist".
Konrad Rudolph
2
@DavidArno Nun, auch ohne DUs gibt es in OOP eine „kanonische“ Lösung dafür, nämlich eine Superklasse von Werten mit Unterklassen für gültige und ungültige Werte. Aber das geht wahrscheinlich zu weit (und in der Praxis scheinen die meisten Codebasen den Polymorphismus der Unterklassen zu vermeiden, anstatt dafür ein Flag zu verwenden, wie in anderen Antworten gezeigt).
Konrad Rudolph
58

Wenn Sie nicht bereits wissen, was eine Monade ist, wäre heute ein großartiger Tag zum Lernen. Ich habe hier eine sanfte Einführung für OO-Programmierer:

https://ericlippert.com/2013/02/21/monads-part-one/

Ihr Szenario ist eine kleine Erweiterung der "vielleicht Monade", auch bekannt als Nullable<T>in C # und Optional<T>in anderen Sprachen.

Angenommen, Sie haben einen abstrakten Typ zur Darstellung der Monade:

abstract class Measurement<T> { ... }

und dann drei Unterklassen:

final class Unknown<T> : Measurement<T> { ... a singleton ...}
final class Empty<T> : Measurement<T> { ... a singleton ... }
final class Actual<T> : Measurement<T> { ... a wrapper around a T ...}

Wir brauchen eine Implementierung von Bind:

abstract class Measurement<T>
{ 
    public Measurement<R> Bind(Func<T, Measurement<R>> f)
  {
    if (this is Unknown<T>) return Unknown<R>.Singleton;
    if (this is Empty<T>) return Empty<R>.Singleton;
    if (this is Actual<T>) return f(((Actual<T>)this).Value);
    throw ...
  }

Daraus können Sie diese vereinfachte Version von Bind schreiben:

public Measurement<R> Bind(Func<A, R> f) 
{
  return this.Bind(a => new Actual<R>(f(a));
}

Und jetzt bist du fertig. Sie haben eine Measurement<int>in der Hand. Du willst es verdoppeln:

Measurement<int> m = whatever;
Measurement<int> doubled = m.Bind(a => a * 2);
Measurement<string> asString = m.Bind(a => a.ToString());

Und folge der Logik; wenn mist Empty<int>dann asStringist Empty<String>, ausgezeichnet.

Ebenso, wenn wir haben

Measurement<int> First()

und

Measurement<double> Second(int i);

dann können wir zwei messungen kombinieren:

Measurement<double> d = First().Bind(Second);

und wieder, wenn First()ist Empty<int>dann dist Empty<double>und so weiter.

Der Schlüsselschritt besteht darin, den Bindevorgang korrekt auszuführen . Überleg es dir gut.

Eric Lippert
quelle
4
Monaden sind (zum Glück) viel einfacher zu gebrauchen als zu verstehen. :)
Guran
11
@leftaroundabout: Gerade weil ich nicht in diese Unterscheidung der Haarspalterei geraten wollte; Wie das Original des Posters festhält, mangelt es vielen Menschen an Selbstvertrauen, wenn es um den Umgang mit Monaden geht. Jargon-beladene Kategorietheoretische Charakterisierungen einfacher Operationen wirken der Entwicklung von Selbstvertrauen und Verständnis entgegen.
Eric Lippert
2
Also Ihr Rat ist , zu ersetzen , Nullmit Nullable+ gewissen Standardcode? :)
Eric Duminil
3
@Claude: Du solltest mein Tutorial lesen. Eine Monade ist ein generischer Typ, der bestimmten Regeln folgt und die Möglichkeit bietet, eine Kette von Operationen zusammenzufügen. In diesem Fall Measurement<T>handelt es sich also um den monadischen Typ.
Eric Lippert
5
@daboross: Obwohl ich der Meinung bin, dass zustandsbehaftete Monaden ein guter Weg sind, um Monaden einzuführen, denke ich nicht, dass der Zustand das ist, was eine Monade auszeichnet. Ich denke an die Tatsache, dass man eine Folge von Funktionen zusammenbinden kann, ist die zwingende Sache; Die Statefulness ist nur ein Implementierungsdetail.
Eric Lippert
18

Ich denke, dass in diesem Fall eine Variation eines Null-Objektmusters nützlich wäre:

public class Measurement
{
    private int value;
    private bool isUnknown = false;
    private bool isMissing = false;

    private Measurement() { }
    public Measurement(int value) { this.value = value; }

    public int Value {
        get {
            if (!isUnknown && !isMissing)
            {
                return this.value;
            }
            throw new SomeException("...");
        }                   
    }

    public static readonly Measurement Unknown = new Measurement
    {
        isUnknown = true
    };

    public static readonly Measurement Missing = new Measurement
    {
        isMissing = true
    };
}

Sie können daraus eine Struktur machen, Equals / GetHashCode / ToString überschreiben, implizite Konvertierungen von oder nach hinzufügen intund, wenn Sie ein NaN-ähnliches Verhalten wünschen, auch Ihre eigenen arithmetischen Operatoren implementieren, so dass z. Measurement.Unknown * 2 == Measurement.Unknown.

Das heißt, C # Nullable<int>implementiert all das, mit der einzigen Einschränkung, dass Sie nicht zwischen verschiedenen Arten von nulls unterscheiden können . Ich bin keine Java-Person, aber ich verstehe, dass Java OptionalIntähnlich ist und andere Sprachen wahrscheinlich ihre eigenen Einrichtungen haben, um einen OptionalTyp darzustellen .

Maciej Stachowski
quelle
6
Die häufigste Implementierung dieses Musters ist die Vererbung. Es kann zwei Unterklassen geben: MissingMeasurement und UnknownMeasurement. Sie können Methoden in der übergeordneten Measurement-Klasse implementieren oder überschreiben. +1
Greg Burghardt
2
Ist es nicht der Punkt des Null-Objektmusters, dass Sie bei ungültigen Werten nicht versagen, sondern nichts tun?
Chris Wohlert
2
@ChrisWohlert in diesem Fall hat das Objekt eigentlich keine Methoden außer dem ValueGetter, was unbedingt fehlschlagen sollte, da man ein UnknownBack nicht in ein konvertieren kann int. Wenn die Messung beispielsweise eine SaveToDatabase()Methode hätte, würde eine gute Implementierung wahrscheinlich keine Transaktion ausführen, wenn das aktuelle Objekt ein Nullobjekt ist (entweder durch Vergleich mit einem Singleton oder durch Überschreiben einer Methode).
Maciej Stachowski
3
@MaciejStachowski Ja, ich sage nicht, dass es nichts tun sollte, ich sage, dass das Null-Objektmuster nicht gut passt. Ihre Lösung ist vielleicht in Ordnung, aber ich würde sie nicht als Nullobjektmuster bezeichnen .
Chris Wohlert
14

Wenn Sie buchstäblich eine ganze Zahl verwenden MÜSSEN, gibt es nur eine mögliche Lösung. Verwenden Sie einige der möglichen Werte als "magische Zahlen", die "fehlende" und "unbekannte" bedeuten.

zB 2,147,483,647 und 2,147,483,646

Wenn Sie das int nur für "echte" Messungen benötigen, erstellen Sie eine kompliziertere Datenstruktur

class Measurement {
    public bool IsEmpty;
    public bool IsKnown;
    public int Value {
        get {
            if(!IsEmpty && IsKnown) return _value;
            throw new Exception("NaN");
            }
        }
}

Wichtige Klarstellung:

Sie können die mathematischen Anforderungen erfüllen, indem Sie die Operatoren für die Klasse überladen

public static Measurement operator+ (Measurement a, Measurement b) {
    if(a.IsEmpty) { return b; }
    ...etc
}
Ewan
quelle
10
@KakturusOption<Option<Int>>
Bergi
5
@Bergi Sie können unmöglich denken, dass das auch nur
annähernd
8
@ BlueRaja-DannyPflughoeft Eigentlich passt es recht gut zur OPs-Beschreibung, die ebenfalls eine verschachtelte Struktur hat. Um akzeptabel zu werden, würden wir natürlich einen richtigen Typalias (oder "neuen Typ") einführen - aber type Measurement = Option<Int>für ein Ergebnis, das eine Ganzzahl oder ein leerer Lesevorgang war, ist dies in Ordnung, ebenso Option<Measurement>für eine Messung, die möglicherweise durchgeführt wurde oder nicht .
Bergi,
7
@arp "Ganzzahlen in der Nähe von NaN"? Können Sie erklären, was Sie damit meinen? Es scheint etwas unerklärlich zu sein, zu sagen, dass eine Zahl dem Begriff „nahe“ ist, dass etwas keine Zahl ist.
Nic Hartley
3
@Nic Hartley In unserem System wurde eine Gruppe von "natürlich" niedrigstmöglichen negativen ganzen Zahlen als NaN reserviert. Wir haben diesen Speicherplatz für die Codierung verschiedener Gründe verwendet, warum diese Bytes etwas anderes als legitime Daten darstellten. (Es war vor Jahrzehnten und ich habe vielleicht einige Details verwischt, aber es gab definitiv eine Reihe von Bits, die Sie in einen ganzzahligen Wert einfügen konnten, um NaN
auszulösen,
11

Wenn Ihre Variablen Gleitkommazahlen sind, hat IEEE754 (der Gleitkommazahlenstandard, der von den meisten modernen Prozessoren und Sprachen unterstützt wird) Ihren Rücken: Es ist eine wenig bekannte Funktion, aber der Standard definiert nicht eine, sondern eine ganze Familie von NaN -Werte (Not-a-Number), die für beliebige anwendungsdefinierte Bedeutungen verwendet werden können. In Gleitkommazahlen mit einfacher Genauigkeit haben Sie beispielsweise 22 freie Bits, mit denen Sie zwischen 2 ^ {22} Typen ungültiger Werte unterscheiden können.

Normalerweise stellen Programmierschnittstellen nur eine von ihnen zur Verfügung (z. B. Numpy's nan). Ich weiß nicht, ob es eine integrierte Möglichkeit gibt, die anderen zu generieren, außer der expliziten Bit-Manipulation, aber es geht nur darum, ein paar Routinen auf niedriger Ebene zu schreiben. (Sie werden auch eine benötigen, um sie voneinander zu unterscheiden, da sie vom Design her a == bimmer false zurückgibt, wenn eine von ihnen eine NaN ist.)

Ihre Verwendung ist besser, als Ihre eigene "magische Zahl" neu zu erfinden, um ungültige Daten zu signalisieren, da sie sich korrekt verbreiten und Ungültigkeit signalisieren: Sie riskieren beispielsweise nicht, sich in den Fuß zu schießen, wenn Sie eine average()Funktion verwenden und vergessen, nach ihnen zu suchen Ihre besonderen Werte.

Das einzige Risiko besteht darin, dass Bibliotheken sie nicht korrekt unterstützen, da sie eine ziemlich unklare Funktion sind: Beispielsweise kann eine Serialisierungsbibliothek sie alle auf dasselbe reduzieren nan(was für die meisten Zwecke äquivalent aussieht).

Federico Poloni
quelle
6

Im Anschluss an David Arno Antwort , können Sie so etwas wie eine diskriminierte Vereinigung in OOP, und in einer objekt funktionale Stil , wie sie tun , indem Scala lieferten, von Java 8 Funktionstypen oder einer Java - FP - Bibliothek wie Vavr oder Fugue es fühlt sich ziemlich natürlich etwas zu schreiben wie:

var value = Measurement.of(2);
out.println(value.map(x -> x * 2));

var empty = Measurement.empty();
out.println(empty.map(x -> x * 2));

var unknown = Measurement.unknown();
out.println(unknown.map(x -> x * 2));

Drucken

Value(4)
Empty()
Unknown()

( Vollständige Implementierung als Kern .)

Eine FP-Sprache oder Bibliothek bietet andere Tools wie Try(aka Maybe) (ein Objekt, das entweder einen Wert oder einen Fehler enthält) und Either(ein Objekt, das entweder einen Erfolgswert oder einen Fehlerwert enthält), die auch hier verwendet werden könnten.

David Moles
quelle
2

Die ideale Lösung für Ihr Problem hängt davon ab, warum Ihnen der Unterschied zwischen einem bekannten Fehler und einer bekannten unzuverlässigen Messung wichtig ist und welche nachgelagerten Prozesse Sie unterstützen möchten. Beachten Sie, dass "nachgelagerte Prozesse" in diesem Fall menschliche Bediener oder andere Entwickler nicht ausschließen.

Das bloße Aufstellen einer "zweiten Variante" von Null gibt den nachgeschalteten Prozessen nicht genügend Informationen, um eine vernünftige Reihe von Verhaltensweisen abzuleiten.

Wenn Sie sich stattdessen auf kontextbezogene Annahmen über die Ursache schlechten Verhaltens stützen, die durch nachgeschalteten Code verursacht werden, würde ich das schlechte Architektur nennen.

Wenn Sie genug wissen, um zwischen einem Grund für einen Fehler und einem Fehler ohne bekannten Grund zu unterscheiden, und diese Informationen künftige Verhaltensweisen beeinflussen, sollten Sie dieses Wissen nachgelagert kommunizieren oder inline verarbeiten.

Einige Muster für den Umgang damit:

  • Summentypen
  • Diskriminierte Gewerkschaften
  • Objekte oder Strukturen mit einer Aufzählung, die das Ergebnis der Operation darstellt, und einem Feld für das Ergebnis
  • Magische Saiten oder magische Zahlen, die im normalen Betrieb nicht zu erreichen sind
  • Ausnahmen in Sprachen, in denen diese Verwendung idiomatisch ist
  • Zu erkennen, dass es eigentlich keinen Wert hat, zwischen diesen beiden Szenarien zu unterscheiden und nur zu verwenden null
Eiserner Gremlin
quelle
2

Wenn es mir darum ging, "etwas zu erledigen", anstatt eine elegante Lösung zu finden, bestand der schnelle und schmutzige Hack darin, einfach die Zeichenfolgen "unbekannt", "fehlend" und "Zeichenfolgendarstellung meines numerischen Werts" zu verwenden, was dann der Fall wäre aus einer Zeichenfolge konvertiert und nach Bedarf verwendet. Schneller als das Schreiben implementiert und zumindest unter Umständen völlig ausreichend. (Ich bilde jetzt einen Wettpool für die Anzahl der Abstimmungen ...)

Mickeyf
quelle
Für die Erwähnung von "etwas erledigen" ausgezeichnet.
Auf Wiedersehen Frau Chipps
4
Einige Leute bemerken möglicherweise, dass dies die meisten Probleme mit der Verwendung von NULL hat, nämlich, dass es nur von NULL-Überprüfungen zu "unbekannten" und "fehlenden" Überprüfungen wechselt, aber den Laufzeitabsturz für die glückliche, stille Datenbeschädigung von beibehält Das Pech als einziger Hinweis, dass Sie einen Scheck vergessen haben. Selbst fehlende NULL-Checks haben den Vorteil, dass Linters sie möglicherweise einfangen, dies jedoch verliert. Es fügt jedoch eine Unterscheidung zwischen "unbekannt" und "vermisst" hinzu, so dass es dort NULL schlägt ...
8bittree
2

Der Kern der Frage lautet: "Wie gebe ich zwei unabhängige Informationen von einer Methode zurück, die ein einzelnes int zurückgibt? Ich möchte meine Rückgabewerte nie überprüfen, und Nullen sind schlecht. Verwenden Sie sie nicht."

Schauen wir uns an, was Sie weitergeben möchten. Sie übergeben entweder eine Ganzzahl oder eine Nicht-Ganzzahl- Begründung dafür, warum Sie die Ganzzahl nicht angeben können. Die Frage besagt, dass es nur zwei Gründe geben wird, aber jeder, der jemals eine Aufzählung gemacht hat, weiß, dass jede Liste wachsen wird. Es ist einfach sinnvoll, andere Gründe anzugeben.

Auf den ersten Blick scheint dies ein guter Fall für das Auslösen einer Ausnahme zu sein.

Wenn Sie dem Aufrufer etwas Besonderes mitteilen möchten, das nicht im Rückgabetyp enthalten ist, sind Ausnahmen häufig das geeignete System: Ausnahmen betreffen nicht nur Fehlerzustände und ermöglichen es Ihnen, eine Vielzahl von Kontexten und Begründungen zurückzugeben, um zu erklären, warum dies nur möglich ist heute nicht.

Und dies ist das EINZIGE System, mit dem Sie garantiert gültige Ints zurückgeben können und das garantiert, dass jeder int-Operator und jede Methode, die Ints akzeptiert, den Rückgabewert dieser Methode akzeptieren kann, ohne jemals nach ungültigen Werten wie null oder magischen Werten suchen zu müssen.

Aber Ausnahmen sind nur dann wirklich eine gültige Lösung, wenn dies, wie der Name schon sagt, ein Ausnahmefall ist und nicht der normale Geschäftsverlauf.

Und ein Try / Catch-and-Handler ist genau so wichtig wie ein Null-Check, gegen den überhaupt Einspruch erhoben wurde.

Und wenn der Anrufer das try / catch nicht enthält, muss der Anrufer das tun und so weiter.


Ein naiver zweiter Durchgang lautet: "Es ist eine Messung. Negative Entfernungsmessungen sind unwahrscheinlich." Für einige Messungen von Y können Sie also nur consts für haben

  • -1 = unbekannt,
  • -2 = unmöglich zu messen,
  • -3 = weigerte sich zu antworten,
  • -4 = bekannt aber vertraulich,
  • -5 = variiert je nach Mondphase, siehe Tabelle 5a,
  • -6 = vierdimensional, Maße im Titel angegeben,
  • -7 = Dateisystem-Lesefehler
  • -8 = reserviert für zukünftige Verwendung,
  • -9 = Quadrat / Kubik, also ist Y dasselbe wie X,
  • -10 = ist ein Monitorbildschirm, daher werden keine X- und Y-Messungen verwendet. Verwenden Sie X als Bildschirmdiagonale.
  • -11 = schrieb die Messungen auf die Rückseite einer Quittung und es wurde in Unleserlichkeit gewaschen, aber ich denke, es war entweder 5 oder 17,
  • -12 = ... du kommst auf die Idee.

Dies ist die Art und Weise, wie dies in vielen alten C-Systemen und sogar in modernen Systemen, in denen es eine echte Einschränkung für int gibt und Sie es nicht in eine Struktur oder eine Monade irgendeines Typs umbrechen können, getan wird.

Wenn die Messungen negativ sein können, vergrößern Sie einfach Ihren Datentyp (z. B. long int) und lassen die magischen Werte höher als den Bereich des int sein. Beginnen Sie im Idealfall mit einem Wert, der in einem Debugger deutlich angezeigt wird.

Es gibt gute Gründe, sie als separate Variable zu haben, anstatt nur magische Zahlen zu haben. Zum Beispiel strikte Typisierung, Wartbarkeit und Erfüllung der Erwartungen.


In unserem dritten Versuch betrachten wir Fälle, in denen es der normale Geschäftsverlauf ist, nicht int-Werte zu haben. Zum Beispiel, wenn eine Auflistung dieser Werte mehrere nicht ganzzahlige Einträge enthalten kann. Dies bedeutet, dass ein Ausnahmebehandler möglicherweise der falsche Ansatz ist.

In diesem Fall ist es ein guter Fall für eine Struktur, die das int und die Begründung übergibt. Wiederum kann diese Begründung nur eine Konstante wie die obige sein, aber anstatt beide im selben int zu halten, speichern Sie sie als verschiedene Teile einer Struktur. Anfangs haben wir die Regel, dass, wenn das Grundprinzip festgelegt ist, das int nicht festgelegt wird. An diese Regel sind wir aber nicht mehr gebunden; Bei Bedarf können wir auch Begründungen für gültige Zahlen liefern.

So oder so, jedes Mal, wenn Sie es aufrufen, benötigen Sie Boilerplate, um das Grundprinzip zu testen, um festzustellen, ob das int gültig ist. Ziehen Sie dann den int-Teil heraus und verwenden Sie ihn, wenn das Grundprinzip dies zulässt.

Hier müssen Sie Ihre Argumentation hinter "Don't Use Null" untersuchen.

Wie Ausnahmen soll Null einen Ausnahmezustand bedeuten.

Wenn ein Aufrufer diese Methode aufruft und den "rationalen" Teil der Struktur vollständig ignoriert, eine Zahl ohne Fehlerbehandlung erwartet und eine Null erhält, behandelt er die Null als Zahl und ist falsch. Wenn es eine magische Zahl erhält, behandelt es das als eine Zahl und ist falsch. Aber wenn es eine Null wird, wird es umfallen , wie es verdammt gut tun sollte.

Jedes Mal, wenn Sie diese Methode aufrufen, müssen Sie den Rückgabewert überprüfen. Sie behandeln jedoch die ungültigen Werte, ob bandintern oder bandextern, try / catch, überprüfen die Struktur auf eine "rationale" Komponente und überprüfen das int nach einer magischen Zahl suchen oder ein Int auf eine Null prüfen ...

Die Alternative, die Multiplikation einer Ausgabe zu behandeln, die ein ungültiges int und eine Begründung wie "Mein Hund hat diese Messung gefressen" enthalten könnte, besteht darin, den Multiplikationsoperator für diese Struktur zu überladen.

... und überladen Sie dann jeden anderen Operator in Ihrer Anwendung, der möglicherweise auf diese Daten angewendet wird.

... und dann alle Methoden überladen, die Ints benötigen könnten.

... Und all diese Überladungen müssen weiterhin Prüfungen auf ungültige Ints enthalten, damit Sie den Rückgabetyp dieser einen Methode so behandeln können, als wäre er zum Zeitpunkt des Aufrufs immer ein gültiger Int.

Die ursprüngliche Prämisse ist also in verschiedener Hinsicht falsch:

  1. Wenn Sie ungültige Werte haben, können Sie es nicht vermeiden, an jedem Punkt im Code, an dem Sie die Werte verarbeiten, nach diesen ungültigen Werten zu suchen.
  2. Wenn Sie etwas anderes als ein int zurückgeben, geben Sie kein int zurück, sodass Sie es nicht wie ein int behandeln können. Das Überladen von Operatoren lässt Sie so tun , als ob , aber das ist nur so, als ob.
  3. Ein Int mit magischen Zahlen (einschließlich NULL, NAN, Inf ...) ist nicht mehr wirklich ein Int, sondern die Struktur eines Armen.
  4. Durch das Vermeiden von Nullen wird der Code nicht robuster. Es werden lediglich die Probleme mit Ints ausgeblendet oder in eine komplexe Ausnahmebehandlungsstruktur verschoben.
Dewi Morgan
quelle
1

Ich verstehe die Prämisse Ihrer Frage nicht, aber hier ist die Nennwertantwort. Für Vermisst oder Leer könnten Sie tun math.nan(keine Zahl). Sie können alle mathematischen Operationen ausführen, math.nanund es wird bleiben math.nan.

Sie können None(Python's null) für einen unbekannten Wert verwenden. Sie sollten sowieso keinen unbekannten Wert manipulieren, und einige Sprachen (Python ist keine von ihnen) haben spezielle Nulloperatoren, sodass die Operation nur ausgeführt wird, wenn der Wert nicht null ist, andernfalls bleibt der Wert null.

Andere Sprachen haben Schutzklauseln (wie Swift oder Ruby) und Ruby hat eine bedingte vorzeitige Rückkehr.

Ich habe dies in Python auf verschiedene Arten gelöst gesehen:

  • bei einer Wrapper-Datenstruktur, da sich numerische Informationen normalerweise auf eine Entität beziehen und eine Messzeit haben. Der Wrapper kann magische Methoden überschreiben, __mult__sodass keine Ausnahmen ausgelöst werden, wenn Ihre Werte für Unbekannt oder Fehlend angezeigt werden. Numpy und Pandas könnten solche Fähigkeiten haben.
  • mit einem Sentinel-Wert (wie Ihr Unknownoder -1 / -2) und einer if-Anweisung
  • mit einer separaten Booleschen Flagge
  • Bei einer verzögerten Datenstruktur führt Ihre Funktion eine Operation an der Struktur aus und gibt dann zurück, dass die äußerste Funktion, die das tatsächliche Ergebnis benötigt, die verzögerte Datenstruktur auswertet
  • mit einer langsamen Pipeline von Operationen - ähnlich der vorherigen, aber diese kann für einen Datensatz oder eine Datenbank verwendet werden
noɥʇʎԀʎzɥʇʎԀʎ
quelle
1

Wie der Wert im Speicher abgelegt wird, hängt von der Sprache und den Implementierungsdetails ab. Ich denke, Sie meinen damit, wie sich das Objekt für den Programmierer verhalten soll. (So ​​lese ich die Frage, sag mir, ob ich falsch liege.)

Sie haben bereits in Ihrer Frage eine Antwort darauf vorgeschlagen: Verwenden Sie Ihre eigene Klasse, die jede mathematische Operation akzeptiert und sich selbst zurückgibt, ohne eine Ausnahme auszulösen. Sie sagen, Sie möchten dies, weil Sie Nullprüfungen vermeiden möchten.

Lösung 1: Vermeiden Sie keine Nullprüfungen

Missingkann dargestellt werden als math.nan
Unknownkann dargestellt werden alsNone

Wenn Sie mehr als einen Wert haben, können Sie filter()die Operation nur auf Werte anwenden, die nicht Unknownoder sind Missing, oder auf Werte, die Sie für die Funktion ignorieren möchten.

Ich kann mir kein Szenario vorstellen, in dem Sie eine Nullprüfung für eine Funktion benötigen, die auf einen einzelnen Skalar wirkt. In diesem Fall empfiehlt es sich, Nullprüfungen zu erzwingen.


Lösung 2: Verwenden Sie einen Dekorateur, der Ausnahmen erkennt

In diesem Fall Missingkönnte ausgelöst werden MissingExceptionund Unknownkönnte ausgelöst werden, UnknownExceptionwenn Operationen daran ausgeführt werden.

@suppressUnknown(value=Unknown) # if an UnknownException is raised, return this value instead
@suppressMissing(value=Missing)
def sigmoid(value):
    ...

Der Vorteil dieses Ansatzes besteht darin, dass die Eigenschaften von Missingund Unknownnur dann unterdrückt werden, wenn Sie ausdrücklich verlangen, dass sie unterdrückt werden. Ein weiterer Vorteil ist, dass dieser Ansatz sich selbst dokumentiert: Jede Funktion zeigt an, ob sie ein Unbekanntes oder ein Fehlendes erwartet und wie die Funktion.

Wenn Sie bei einem Funktionsaufruf nicht erwarten, dass eine fehlende Nachricht eine fehlende Nachricht enthält, wird die Funktion sofort ausgelöst und zeigt Ihnen genau, wo der Fehler aufgetreten ist, anstatt stillschweigend eine fehlende Nachricht in der Aufrufkette weiterzugeben. Gleiches gilt für Unknown.

sigmoidkann immer noch anrufen sin, auch wenn es kein Missingoder erwartet Unknown, da sigmoidder dekorateur die ausnahme abfängt.

noɥʇʎԀʎzɥʇʎԀʎ
quelle
1
frage mich, was der Sinn ist, zwei Antworten auf die gleiche Frage zu posten (dies ist Ihre vorherige Antwort , stimmt etwas nicht?)
Mücke
@gnat Diese Antwort liefert eine Begründung, warum dies nicht so erfolgen sollte, wie es der Autor zeigt, und ich wollte nicht die Mühe auf sich nehmen, zwei Antworten mit unterschiedlichen Ideen zu integrieren - es ist einfach einfacher, zwei Antworten zu schreiben, die unabhängig voneinander gelesen werden können . Ich verstehe nicht, warum Ihnen das harmlose Denken eines anderen so wichtig ist.
noɥʇʎԀʎzɥʇʎԀʎ
0

Es sei angenommen, dass die Anzahl der CPUs in einem Server abgerufen wird. Wenn der Server ausgeschaltet ist oder verschrottet wurde, existiert dieser Wert einfach nicht. Es wird eine Messung sein, die keinen Sinn ergibt ("fehlt" / "leer" sind möglicherweise nicht die besten Begriffe). Aber der Wert ist "bekannt" als unsinnig. Wenn der Server vorhanden ist, der Vorgang zum Abrufen des Werts jedoch abstürzt, ist die Messung gültig, schlägt jedoch fehl und führt zu einem "unbekannten" Wert.

Beides klingt nach Fehlerzuständen, daher würde ich beurteilen, dass die beste Option hier darin besteht get_measurement(), beide sofort als Ausnahmen zu werfen (wie z. B. DataSourceUnavailableExceptionoder SpectacularFailureToGetDataException). Wenn eines dieser Probleme auftritt, kann der Datenerfassungscode sofort darauf reagieren (z. B. indem er es im letzteren Fall erneut versucht) und muss get_measurement()nur einen zurückgeben, intwenn die Daten erfolgreich aus den Daten abgerufen werden können Quelle - und Sie wissen, dass das intgültig ist.

Wenn Ihre Situation keine Ausnahmen unterstützt oder diese nicht in großem Umfang nutzt, ist die Verwendung von Fehlercodes eine gute Alternative, die möglicherweise über eine separate Ausgabe an zurückgegeben werden get_measurement(). Dies ist das idiomatische Muster in C, in dem die tatsächliche Ausgabe in einem Eingabezeiger gespeichert und ein Fehlercode als Rückgabewert zurückgegeben wird.

Der Hansinator
quelle
0

Die gegebenen Antworten sind in Ordnung, spiegeln aber immer noch nicht die hierarchische Beziehung zwischen Wert, leer und unbekannt wider.

  • Höchste kommt unbekannt .
  • Dann muss vor Verwendung eines Wertes zunächst leer geklärt werden.
  • Zuletzt kommt der Wert , mit dem gerechnet werden soll.

Hässlich (wegen der fehlgeschlagenen Abstraktion), aber voll funktionsfähig wäre (in Java):

Optional<Optional<Integer>> unknowableValue;

unknowableValue.ifPresent(emptiableValue -> ...);
Optional<Integer> emptiableValue = unknowableValue.orElse(Optional.empty());

emptiableValue.ifPresent(value -> ...);
int value = emptiableValue.orElse(0);

Hier sind funktionale Sprachen mit einem schönen Typensystem besser.

Tatsächlich: Die leeren / fehlenden und unbekannten * Nicht-Werte scheinen eher Teil eines Prozesszustands, einer Produktionspipeline zu sein. Wie Excel-Tabellenkalkulationszellen mit Formeln, die auf andere Zellen verweisen. Dort würde man vielleicht an das Speichern von kontextbezogenen Lambdas denken. Das Ändern einer Zelle würde alle rekursiv abhängigen Zellen neu bewerten.

In diesem Fall würde ein int-Wert von einem int-Lieferanten erhalten. Ein leerer Wert würde bewirken, dass ein int-Lieferant eine leere Ausnahme auslöst oder leer wird (rekursiv nach oben). Ihre Hauptformel würde alle Werte verbinden und möglicherweise auch ein Leerzeichen (Wert / Ausnahme) zurückgeben. Ein unbekannter Wert würde die Auswertung durch Auslösen einer Ausnahme deaktivieren.

Werte wären wahrscheinlich beobachtbar, wie eine Java-gebundene Eigenschaft, die die Listener über Änderungen benachrichtigt.

Kurz gesagt: Das wiederkehrende Muster, dass Werte mit zusätzlichen leeren und unbekannten Zuständen benötigt werden, scheint darauf hinzudeuten, dass ein Datenmodell mit mehr Tabellenkalkulation wie gebundenen Eigenschaften möglicherweise besser ist.

Joop Eggen
quelle
0

Ja, das Konzept mehrerer verschiedener NA-Typen existiert in einigen Sprachen. Dies gilt umso mehr für statistische Fälle, in denen dies aussagekräftiger ist (d. h. die enorme Unterscheidung zwischen " Missing-At-Random", "Missing-Completely-At-Random" und "Missing-Not-At-Random" ).

  • Wenn wir nur Widgetlängen messen, ist es nicht entscheidend, zwischen "Sensorausfall" oder "Stromausfall" oder "Netzwerkausfall" zu unterscheiden (obwohl "numerischer Überlauf" Informationen übermittelt).

  • B. beim Data Mining oder bei einer Umfrage, bei der die Befragten nach ihrem Einkommen oder dem HIV-Status gefragt werden, unterscheidet sich das Ergebnis von "Unbekannt" von "Antwort ablehnen", und Sie können sehen, dass unsere vorherigen Annahmen darüber, wie letzteres unterstellt werden soll, eher zutreffen anders sein als die ersteren. Sprachen wie SAS unterstützen also mehrere verschiedene NA-Typen. die R-Sprache nicht, aber die Benutzer müssen sich sehr oft darum kümmern; NAs an verschiedenen Punkten in einer Pipeline können verwendet werden, um sehr unterschiedliche Dinge zu bezeichnen.

  • Es gibt auch den Fall, dass wir mehrere NA-Variablen für einen einzelnen Eintrag haben ("Multiple Imputation"). Beispiel: Wenn ich das Alter, die Postleitzahl, das Bildungsniveau oder das Einkommen einer Person nicht kenne, ist es schwieriger, deren Einkommen anzurechnen.

In Bezug auf die Darstellung verschiedener NA-Typen in Mehrzwecksprachen, die diese nicht unterstützen, hacken die Leute im Allgemeinen Dinge wie Gleitkomma-NaN (erfordert die Konvertierung von Ganzzahlen), Enums oder Sentinels (z. B. 999 oder -1000) für Ganzzahlen oder kategoriale Werte. Normalerweise gibt es keine sehr klare Antwort, sorry.

smci
quelle
0

R verfügt über eine integrierte Unterstützung für fehlende Werte. https://medium.com/coinmonks/dealing-with-missing-data-using-r-3ae428da2d17

Edit: weil ich downvoted wurde, werde ich ein bisschen erklären.

Wenn Sie sich mit Statistik befassen, empfehle ich die Verwendung einer Statistiksprache wie R, da R von Statistikern für Statistiker geschrieben wird. Fehlende Werte sind ein so großes Thema, dass sie Ihnen ein ganzes Semester lang beibringen. Und es gibt große Bücher nur über fehlende Werte.

Sie können jedoch fehlende Daten markieren, z. B. Punkte oder "fehlende" oder was auch immer. In R können Sie definieren, was Sie mit Vermissen meinen. Sie müssen sie nicht konvertieren.

Der normale Weg, einen fehlenden Wert zu definieren, besteht darin, ihn als zu markieren NA.

x <- c(1, 2, NA, 4, "")

Dann können Sie sehen, welche Werte fehlen.

is.na(x)

Und dann wird das Ergebnis sein;

FALSE FALSE  TRUE FALSE FALSE

Wie Sie sehen können, ""fehlt es nicht. Sie können ""als unbekannt bedrohen . Und NAfehlt.

ilhan
quelle
@Hulk, welche anderen funktionalen Sprachen unterstützen fehlende Werte? Selbst wenn sie fehlende Werte unterstützen, können Sie sie sicher nicht mit statistischen Methoden in nur einer Codezeile füllen.
Ilhan
-1

Gibt es einen Grund, warum die Funktionalität des *Bedieners nicht geändert werden kann?

Bei den meisten Antworten handelt es sich um eine Art Nachschlagewert. In diesem Fall ist es jedoch möglicherweise einfacher, den mathematischen Operator zu ändern.

Sie würden dann ähnlich zu haben , in der Lage empty()/ unknown()Funktionalität in Ihrem gesamten Projekt.

Edward
quelle
4
Dies bedeutet, dass Sie alle Operatoren überlasten müssten
Pipe