Generische Mathematik

In .NET 7 werden neue mathematikbezogene generische Schnittstellen für die Basisklassenbibliothek eingeführt. Dank dieser Schnittstellen können Sie einen Typparameter eines generischen Typs oder einer generischen Methode so beschränken, dass er „zahlenähnlich“ ist. Darüber hinaus können Sie ab C# 11 Schnittstellenmember vom Typ static virtual definieren. Da Operatoren als static deklariert werden müssen, können mit diesem neuen C#-Feature Operatoren in den neuen Schnittstellen für zahlenähnliche Typen deklariert werden.

Durch die Kombination dieser Innovationen können mathematische Operationen generisch (also ohne Kenntnis des genauen Typs, mit dem Sie arbeiten) ausgeführt werden. Wenn Sie beispielsweise eine Methode schreiben möchten, die zwei Zahlen addiert, mussten Sie bislang für jeden Typ eine Überladung der Methode hinzufügen (beispielsweise static int Add(int first, int second) und static float Add(float first, float second)). Jetzt können Sie eine einzelne generische Methode schreiben, bei der der Typparameter auf einen zahlenähnlichen Typ beschränkt ist. Beispiel:

static T Add<T>(T left, T right)
    where T : INumber<T>
{
    return left + right;
}

In dieser Methode ist der Typparameter T auf einen Typ beschränkt, der die neue Schnittstelle INumber<TSelf> implementiert. INumber<TSelf> implementiert die Schnittstelle IAdditionOperators<TSelf,TOther,TResult>, die den +-Operator enthält. Dadurch kann die Methode die beiden Zahlen generisch addieren. Die Methode kann mit jedem der integrierten numerischen Typen von .NET verwendet werden, da sie in .NET 7 alle für die Implementierung von INumber<TSelf> aktualisiert wurden.

Bibliotheksautor*innen profitieren am meisten von den generischen mathematischen Schnittstellen, da sie ihre Codebasis vereinfachen können, indem sie redundante Überladungen entfernen. Andere Entwickler*innen profitieren indirekt, da die von ihnen genutzten APIs künftig ggf. mehr Typen unterstützen.

Die Schnittstellen

Die Schnittstellen sind differenziert genug, dass Benutzer*innen auf ihrer Grundlage eigene Schnittstellen definieren können, gleichzeitig aber auch so präzise, dass sie problemlos genutzt werden können. Es gibt einige wichtige numerische Schnittstellen, mit denen die meisten Benutzer interagieren – beispielsweise INumber<TSelf> und IBinaryInteger<TSelf>. Die differenzierteren Schnittstellen wie IAdditionOperators<TSelf,TOther,TResult> und ITrigonometricFunctions<TSelf>unterstützen diese Typen und sind für Entwickler*innen verfügbar, die ihre eigenen domänenspezifischen numerischen Schnittstellen definieren.

Numerische Schnittstellen

Dieser Abschnitt enthält Informationen zu den Schnittstellen in System.Numerics, die zahlenähnliche Typen beschreiben, sowie zu den ihnen zur Verfügung stehenden Funktionen.

Schnittstellenname Beschreibung
IBinaryFloatingPointIeee754<TSelf> Macht APIs verfügbar, die üblicherweise für binäre Gleitkommatypen1 verwendet werden, die den IEEE 754-Standard implementieren.
IBinaryInteger<TSelf> Macht APIs verfügbar, die üblicherweise für binäre ganze Zahlen2 verwendet werden.
IBinaryNumber<TSelf> Macht APIs verfügbar, die üblicherweise für binäre Zahlen verwendet werden.
IFloatingPoint<TSelf> Macht APIs verfügbar, die üblicherweise für Gleitkommatypen verwendet werden.
IFloatingPointIeee754<TSelf> Macht APIs verfügbar, die üblicherweise für Gleitkommatypen verwendet werden, die den IEEE 754-Standard implementieren.
INumber<TSelf> Macht APIs verfügbar, die üblicherweise für vergleichbare Zahlentypen (faktisch die Domäne reeller Zahlen) verwendet werden.
INumberBase<TSelf> Macht APIs verfügbar, die üblicherweise für alle Zahlentypen (faktisch die Domäne komplexer Zahlen) verwendet werden.
ISignedNumber<TSelf> Macht APIs verfügbar, die üblicherweise für alle Zahlentypen mit Vorzeichen verwendet werden (beispielsweise für das Konzept NegativeOne).
IUnsignedNumber<TSelf> Macht APIs verfügbar, die üblicherweise für alle Zahlentypen ohne Vorzeichen verwendet werden.
IAdditiveIdentity<TSelf,TResult> Macht das Konzept (x + T.AdditiveIdentity) == x verfügbar.
IMinMaxValue<TSelf> Macht die Konzepte T.MinValue und T.MaxValue verfügbar.
IMultiplicativeIdentity<TSelf,TResult> Macht das Konzept (x * T.MultiplicativeIdentity) == x verfügbar.

1 Die binären Gleitkommatypen sind Double (double), Half und Single (float).

2 Die binären Ganzzahltypen sind Byte (byte), Int16 (short), Int32 (int), Int64 (long), Int128, IntPtr (nint), SByte (sbyte), UInt16 (ushort), UInt32 (uint), UInt64 (ulong), UInt128 und UIntPtr (nuint).

Die Schnittstelle, die Sie am wahrscheinlichsten direkt verwenden, ist INumber<TSelf>, was in etwa einer reellen Zahl entspricht. Wenn ein Typ diese Schnittstelle implementiert, bedeutet das, dass ein Wert über ein Vorzeichen verfügt (was unsigned-Typen einschließt, da sie als positiv betrachtet werden) und dass er mit anderen Werten gleichen Typs verglichen werden kann. INumberBase<TSelf> bietet erweiterte Konzepte wie komplexe und imaginäre Zahlen – beispielsweise die Quadratwurzel einer negativen Zahl. Andere Schnittstellen (etwa IFloatingPointIeee754<TSelf>) wurden erstellt, da nicht alle Operationen für alle Zahlentypen sinnvoll sind. So ist beispielsweise die Berechnung der Untergrenze einer Zahl nur bei Gleitkommatypen sinnvoll. In der .NET-Basisklassenbibliothek wird IFloatingPointIeee754<TSelf> vom Gleitkommatyp Double implementiert, Int32 aber nicht.

Mehrere der Schnittstellen werden auch von verschiedenen anderen Typen implementiert, darunter Char, DateOnly, DateTime, DateTimeOffset, Decimal, Guid, TimeOnly und TimeSpan.

Die folgende Tabelle enthält einige der wichtigsten APIs, die von den einzelnen Schnittstellen verfügbar gemacht werden:

Schnittstelle API-Name Beschreibung
IBinaryInteger<TSelf> DivRem Berechnet gleichzeitig den Quotienten und den Rest.
LeadingZeroCount Ermittelt die Anzahl führender Nullbits in der Binärdarstellung.
PopCount Ermittelt die Anzahl festgelegter Bits in der Binärdarstellung.
RotateLeft Rotiert Bits nach links, was manchmal auch als kreisförmige Verschiebung nach links bezeichnet wird.
RotateRight Rotiert Bits nach rechts, was manchmal auch als kreisförmige Verschiebung nach rechts bezeichnet wird.
TrailingZeroCount Ermittelt die Anzahl nachfolgender Nullbits in der Binärdarstellung.
IFloatingPoint<TSelf> Ceiling Rundet den Wert in Richtung positive Unendlichkeit. +4,5 wird zu +5, und -4,5 wird zu -4.
Floor Rundet den Wert in Richtung negative Unendlichkeit. +4,5 wird zu +4, und -4,5 wird zu -5.
Round Rundet den Wert mit dem angegebenen Rundungsmodus.
Truncate Rundet den Wert in Richtung null. +4,5 wird zu +4, und -4,5 wird zu -4.
IFloatingPointIeee754<TSelf> E Ruft einen Wert ab, der die eulersche Zahl für den Typ darstellt.
Epsilon Ruft den kleinsten darstellbaren Wert ab, der für den Typ größer null ist.
NaN Ruft einen Wert ab, der NaN für den Typ darstellt.
NegativeInfinity Ruft einen Wert ab, der -Infinity für den Typ darstellt.
NegativeZero Ruft einen Wert ab, der -Zero für den Typ darstellt.
Pi Ruft einen Wert ab, der Pi für den Typ darstellt.
PositiveInfinity Ruft einen Wert ab, der +Infinity für den Typ darstellt.
Tau Ruft einen Wert ab, der Tau (2 * Pi) für den Typ darstellt.
(Sonstige) (Implementiert die vollständige Gruppe von Schnittstellen, die unter Funktionsschnittstellen aufgeführt sind.)
INumber<TSelf> Clamp Beschränkt einen Wert so, dass er nicht größer als der angegebene Maximalwert und nicht kleiner als der angegebene Mindestwert ist.
CopySign Legt das Vorzeichen eines angegebenen Werts auf das Vorzeichen eines anderen angegebenen Werts fest.
Max Gibt den größeren von zwei Werten zurück. Ist eine der beiden Eingaben NaN, wird NaN zurückgegeben.
MaxNumber Gibt den größeren von zwei Werten zurück. Ist eine Eingabe NaN, wird die Zahl zurückgegeben.
Min Gibt den kleineren von zwei Werten zurück. Ist eine der beiden Eingaben NaN, wird NaN zurückgegeben.
MinNumber Gibt den kleineren von zwei Werten zurück. Ist eine Eingabe NaN, wird die Zahl zurückgegeben.
Sign Gibt „-1“ für negative Werte, „0“ für null und „+1“ für positive Werte zurück.
INumberBase<TSelf> One Ruft den Wert „1“ für den Typ ab.
Radix Ruft die Basis für den Typ ab. Int32 gibt „2“ zurück. Decimal gibt „10“ zurück.
Zero Ruft den Wert „0“ für den Typ ab.
CreateChecked Erstellt einen Wert, und löst eine Überlaufausnahme (OverflowException) aus, wenn der Platz für die Eingabe nicht ausreicht.1
CreateSaturating Erstellt einen Wert, und führt ein Clamping auf T.MinValue oder T.MaxValue durch, wenn der Platz für die Eingabe nicht ausreicht.1
CreateTruncating Erstellt einen Wert auf der Grundlage eines anderen Werts, und bricht ihn um, wenn der Platz für die Eingabe nicht ausreicht.1
IsComplexNumber Gibt „true“ zurück, wenn der Wert einen reellen Teil ungleich null und einen imaginären Teil ungleich null hat.
IsEvenInteger Gibt „true“ zurück, wenn der Wert eine gerade ganze Zahl ist. Für „2,0“ wird true zurückgegeben. Für „2,2“ wird false zurückgegeben.
IsFinite Gibt „true“ zurück, wenn der Wert nicht unendlich und nicht NaN ist.
IsImaginaryNumber Gibt „true“ zurück, wenn der Wert einen reellen Teil hat, der null ist. Das bedeutet: 0 ist imaginär, 1 + 1i nicht.
IsInfinity Gibt „true“ zurück, wenn der Wert Unendlichkeit darstellt.
IsInteger Gibt „true“ zurück, wenn der Wert eine ganze Zahl ist. Für „2,0“ und „3,0“ wird true zurückgegeben. Für „2,2“ und „3,1“ wird false zurückgegeben.
IsNaN Gibt „true“ zurück, wenn der Wert NaN darstellt.
IsNegative Gibt „true“ zurück, wenn der Wert negativ ist. Dies schließt „-0,0“ ein.
IsPositive Gibt „true“ zurück, wenn der Wert positiv ist. Dies schließt „0“ und „+0,0“ ein.
IsRealNumber Gibt „true“ zurück, wenn der Wert einen imaginären Teil hat, der null ist. Das bedeutet, dass „0“ sowie alle INumber<T>-Typen reell sind.
IsZero Gibt „true“ zurück, wenn der Wert null darstellt. Dies schließt „0“, „+0,0“ und „-0,0“ ein.
MaxMagnitude Gibt den Wert mit einem höheren absoluten Wert zurück. Ist eine der Eingaben NaN, wird NaN zurückgegeben.
MaxMagnitudeNumber Gibt den Wert mit einem höheren absoluten Wert zurück. Ist eine Eingabe NaN, wird die Zahl zurückgegeben.
MinMagnitude Gibt den Wert mit einem niedrigeren absoluten Wert zurück. Ist eine der Eingaben NaN, wird NaN zurückgegeben.
MinMagnitudeNumber Gibt den Wert mit einem niedrigeren absoluten Wert zurück. Ist eine Eingabe NaN, wird die Zahl zurückgegeben.
ISignedNumber<TSelf> NegativeOne Ruft den Wert „-1“ für den Typ ab.

1 Sehen Sie sich zum besseren Verständnis des Verhaltens der drei Create*-Methoden die folgenden Beispiele an:

Beispiel für einen zu großen Wert:

  • byte.CreateChecked(384) löst eine Überlaufausnahme (OverflowException) aus.
  • byte.CreateSaturating(384) gibt „255“ zurück, da 384 größer als Byte.MaxValue (255) ist.
  • byte.CreateTruncating(384) gibt „128“ zurück, da die niedrigsten 8 Bits verwendet werden. (Die hexadezimale Darstellung von 384 ist 0x0180, und die niedrigsten 8 Bits sind 0x80, was 128 entspricht.)

Beispiel für einen zu kleinen Wert:

  • byte.CreateChecked(-384) löst eine Überlaufausnahme (OverflowException) aus.
  • byte.CreateSaturating(-384) gibt „0“ zurück, da -384 kleiner als Byte.MinValue (0) ist.
  • byte.CreateTruncating(-384) gibt „128“ zurück, da die niedrigsten 8 Bits verwendet werden. (Die hexadezimale Darstellung von 384 ist 0xFE80, und die niedrigsten 8 Bits sind 0x80, was 128 entspricht.)

Bei den Create*-Methoden müssen auch einige besondere Aspekte für IEEE 754-Gleitkommatypen wie float und double berücksichtigt werden, da sie über die speziellen Werte PositiveInfinity, NegativeInfinity und NaN verfügen. Alle drei Create*-APIs verhalten sich wie CreateSaturating. MinValue und MaxValue stellen zwar die größte negative bzw. positive normale Zahl dar, die tatsächlichen Minimal- und Maximalwerte sind jedoch NegativeInfinity und PositiveInfinity, sodass stattdessen ein Clamping auf diese Werte durchgeführt wird.

Operatorschnittstellen

Die Operatorschnittstellen entsprechen den verschiedenen Operatoren, die für die Programmiersprache C# verfügbar sind.

  • Operationen wie Multiplikation und Division werden explizit nicht gekoppelt, da dies nicht für alle Typen korrekt ist. Matrix4x4 * Matrix4x4 ist beispielsweise gültig, Matrix4x4 / Matrix4x4 aber nicht.
  • Die Eingabe- und Ergebnistypen können sich üblicherweise unterscheiden, um Szenarien wie das Dividieren zweier ganzer Zahlen zu unterstützen, um einen double-Wert (beispielsweise 3 / 2 = 1.5) zu erhalten, oder um die Berechnung des Durchschnitts einer Reihe ganzer Zahlen zu ermöglichen.
Schnittstellenname Definierte Operatoren
IAdditionOperators<TSelf,TOther,TResult> x + y
IBitwiseOperators<TSelf,TOther,TResult> x & y, 'x | y', x ^ y und ~x
IComparisonOperators<TSelf,TOther,TResult> x < y, x > y, x <= y und x >= y
IDecrementOperators<TSelf> --x und x--
IDivisionOperators<TSelf,TOther,TResult> x / y
IEqualityOperators<TSelf,TOther,TResult> x == y und x != y
IIncrementOperators<TSelf> ++x und x++
IModulusOperators<TSelf,TOther,TResult> x % y
IMultiplyOperators<TSelf,TOther,TResult> x * y
IShiftOperators<TSelf,TOther,TResult> x << y und x >> y
ISubtractionOperators<TSelf,TOther,TResult> x - y
IUnaryNegationOperators<TSelf,TResult> -x
IUnaryPlusOperators<TSelf,TResult> +x

Hinweis

Einige der Schnittstellen definieren neben einem regulären, nicht überprüften Operator auch einen überprüften Operator. Überprüfte Operatoren werden in überprüften Kontexten aufgerufen und ermöglichen es einem benutzerdefinierten Typ, das Überlaufverhalten zu definieren. Wenn Sie einen überprüften Operator (z. B. CheckedSubtraction(TSelf, TOther)) implementieren, müssen Sie auch den nicht überprüften Operator (z. B. Subtraction(TSelf, TOther)) implementieren.

Funktionsschnittstellen

Die Funktionsschnittstellen definieren allgemeine mathematische APIs, die weiter gefasst sind und nicht nur für eine spezifische numerische Schnittstelle gelten. Diese Schnittstellen werden alle von IFloatingPointIeee754<TSelf> implementiert. Möglicherweise werden sie in Zukunft auch von anderen relevanten Typen implementiert.

Schnittstellenname Beschreibung
IExponentialFunctions<TSelf> Macht Exponentialfunktionen verfügbar, die e^x, e^x - 1, 2^x, 2^x - 1, 10^x und 10^x - 1 unterstützen.
IHyperbolicFunctions<TSelf> Macht Hyperbelfunktionen verfügbar, die acosh(x), asinh(x), atanh(x), cosh(x), sinh(x) und tanh(x) unterstützen.
ILogarithmicFunctions<TSelf> Macht Logarithmusfunktionen verfügbar, die ln(x), ln(x + 1), log2(x), log2(x + 1), log10(x) und log10(x + 1) unterstützen.
IPowerFunctions<TSelf> Macht Potenzfunktionen verfügbar, die x^y unterstützen.
IRootFunctions<TSelf> Macht Wurzelfunktionen verfügbar, die cbrt(x) und sqrt(x) unterstützen.
ITrigonometricFunctions<TSelf> Macht trigonometrische Funktionen verfügbar, die acos(x), asin(x), atan(x), cos(x), sin(x) und tan(x) unterstützen.

Analyse- und Formatierungsschnittstellen

Analyse und Formatierung sind zentrale Konzepte der Programmierung. Sie werden häufig beim Konvertieren von Benutzereingaben in einen bestimmten Typ oder beim Anzeigen eines Typs für Benutzer*innen verwendet. Diese Schnittstellen befinden sich im Namespace System.

Schnittstellenname Beschreibung
IParsable<TSelf> Macht die Unterstützung von T.Parse(string, IFormatProvider) und T.TryParse(string, IFormatProvider, out TSelf) verfügbar.
ISpanParsable<TSelf> Macht die Unterstützung von T.Parse(ReadOnlySpan<char>, IFormatProvider) und T.TryParse(ReadOnlySpan<char>, IFormatProvider, out TSelf) verfügbar.
IFormattable1 Macht die Unterstützung von value.ToString(string, IFormatProvider) verfügbar.
ISpanFormattable1 Macht die Unterstützung von value.TryFormat(Span<char>, out int, ReadOnlySpan<char>, IFormatProvider) verfügbar.

1 Diese Schnittstelle ist weder neu noch generisch. Sie wird jedoch von allen Zahlentypen implementiert und stellt die Umkehroperation zu IParsable dar.

Das folgende Programm verwendet beispielsweise zwei Zahlen als Eingabe und liest sie mithilfe einer generischen Methode, bei der der Typparameter auf IParsable<TSelf> beschränkt ist, aus der Konsole. Es berechnet den Durchschnitt mithilfe einer generischen Methode, bei der die Typparameter für die Eingabe- und Ergebniswerte auf INumber<TSelf> beschränkt sind, und zeigt das Ergebnis dann in der Konsole an.

using System.Globalization;
using System.Numerics;

static TResult Average<T, TResult>(T first, T second)
    where T : INumber<T>
    where TResult : INumber<TResult>
{
    return TResult.CreateChecked( (first + second) / T.CreateChecked(2) );
}

static T ParseInvariant<T>(string s)
    where T : IParsable<T>
{
    return T.Parse(s, CultureInfo.InvariantCulture);
}

Console.Write("First number: ");
var left = ParseInvariant<float>(Console.ReadLine());

Console.Write("Second number: ");
var right = ParseInvariant<float>(Console.ReadLine());

Console.WriteLine($"Result: {Average<float, float>(left, right)}");

/* This code displays output similar to:

First number: 5.0
Second number: 6
Result: 5.5
*/

Siehe auch