Share via


Untertypisierung und Varianz

Q# unterstützt nur einige wenige Konvertierungsmechanismen. Implizite Konvertierungen können nur beim Anwenden binärer Operatoren, beim Auswerten bedingter Ausdrücke oder beim Erstellen eines Arrayliterals durchgeführt werden. In diesen Fällen wird ein gemeinsamer Übertyp bestimmt, und die erforderlichen Konvertierungen werden automatisch ausgeführt. Abgesehen von solchen impliziten Konvertierungen sind explizite Konvertierungen über Funktionsaufrufe möglich und oft erforderlich.

Derzeit gilt die einzige vorhandene Untertypisierungsbeziehung für Vorgänge. Intuitiv sollte es möglich sein, einen Vorgang zu ersetzen, der mehr als die erforderliche Gruppe von Funktionselementen unterstützt. Für zwei konkrete Typen, TIn und TOut, ist die Untertypisierungsbeziehung

    (TIn => TOut) :>
    (TIn => TOut is Adj), (TIn => TOut is Ctl) :>
    (TIn => TOut is Adj + Ctl)

wobei A :> B angibt, dass B ein Untertyp von A ist. Anders formuliert: B ist restriktiver als A, sodass ein Wert vom Typ B überall dort verwendet werden kann, wo ein Wert vom Typ A erforderlich ist. Wenn eine aufrufbare Variable auf einem Argument (Element) vom Typ A basiert, kann ein Argument vom Typ B sicher ersetzt werden, da es alle erforderlichen Funktionen bereitstellt.

Diese Art von Polymorphie erstreckt sich auf Tupel, da ein Tupel vom Typ B ein Untertyp eines Tupeltyps (A) ist, wenn er die gleiche Anzahl von Elementen enthält und der Typ jedes Elements ein Untertyp des entsprechenden Elementtyps in A ist. Dies wird als Tiefenuntertypisierung bezeichnet. Die Breitenuntertypisierung wird derzeit nicht unterstützt. Es gibt also keine Untertypbeziehung zwischen zwei benutzerdefinierten Typen oder einem benutzerdefinierten Typ und einem integrierten Typ. Das Vorhandensein des unwrap-Operators, der das Extrahieren eines Tupels mit allen benannten und anonymen Elementen ermöglicht, verhindert dies.

Hinweis

Wenn eine aufrufbare Komponente ein Argument vom Typ A verarbeitet, kann sie auch ein Argument vom Typ B verarbeiten. Wenn eine aufrufbare Komponente als Argument an eine andere aufrufbare Komponente übergeben wird, muss sie in der Lage sein, alles zu verarbeiten, was die Typsignatur ggf. erfordert. Das bedeutet, dass jede aufrufbare Komponente, die ein allgemeineres Argument vom Typ A verarbeiten kann, problemlos übergeben werden kann, wenn die aufrufbare Komponente ein Argument vom Typ B verarbeiten können muss. Umgekehrt erwarten wir, dass die Zusage, einen Wert vom Typ A zurückzugeben, ausreichend ist, wenn die übergebene aufrufbare Komponente einen Wert vom Typ B zurückgibt, da dieser Wert alle erforderlichen Funktionen bereitstellt.

Der Vorgangs- oder Funktionstyp ist im Argumenttyp kontravariant und im Rückgabetyp kovariant. A :> B impliziert daher, dass für jeden konkreten Typ T1,

    (B → T1) :> (A → T1), and
    (T1 → A) :> (T1 → B) 

wo hier entweder eine Funktion oder ein Vorgang bedeuten kann, und wir lassen Anmerkungen für Merkmale weg. Das Ersetzen von A durch (B → T2) bzw. (T2 → A) und das Ersetzen von B durch (A → T2) bzw. (T2 → B) führt zu der Schlussfolgerung, dass für jeden konkreten Typ T2 Folgendes gilt:

    ((A → T2) → T1) :> ((B → T2) → T1), and
    ((T2 → B) → T1) :> ((T2 → A) → T1), and
    (T1 → (B → T2)) :> (T1 → (A → T2)), and
    (T1 → (T2 → A)) :> (T1 → (T2 → B)) 

Daraus folgt im Rückschluss, dass jede zusätzliche Umleitung die Varianz des Argumenttyps umkehrt und die Varianz des Rückgabetyps unverändert lässt.

Hinweis

Dadurch wird auch deutlich, was das Varianzverhalten von Arrays sein muss. Das Abrufen von Elementen über einen Elementzugriffsoperator entspricht dem Aufrufen einer Funktion vom Typ (Int -> TItem), wobei TItem der Typ der Elemente im Array ist. Da diese Funktion bei der Übergabe eines Arrays implizit übergeben wird, folgt, dass Arrays in ihrem Elementtyp kovariant sein müssen. Die gleichen Überlegungen gelten auch für Tupel, die in Bezug auf jeden Elementtyp unveränderlich und daher kovariant sind. Wenn Arrays nicht unveränderlich wären, würde das Vorhandensein eines Konstrukts, das das Festlegen von Elementen in einem Array ermöglicht und somit ein Argument vom Typ TItem verwendet, implizieren, dass Arrays auch kontravariant sein müssen. Die einzige Option für Datentypen, die das Abrufen und Festlegen von Elementen unterstützen, besteht daher darin, invariant zu sein, was bedeutet, dass es überhaupt keine Untertypbeziehung gibt. B[] ist kein Untertyp von A[], auch wenn B ein Untertyp von A ist. Ungeachtet der Tatsache, dass Arrays in Q#unveränderlich sind, sind sie eher invariant als kovariant. Dies bedeutet z. B., dass ein Wert vom Typ (Qubit => Unit is Adj)[] nicht an eine aufrufbare Komponente übergeben werden kann, die ein Argument vom Typ (Qubit => Unit)[] erfordert. Die Beibehaltung der Invarianz von Arrays ermöglicht mehr Flexibilität in Bezug auf die Behandlung und Optimierung von Arrays in der Laufzeit. aber in Zukunft ist es vielleicht möglich, dies zu korrigieren.