Deep Null Checking, gibt es einen besseren Weg?


129

Hinweis: Diese Frage vor der Einführung der gebeten wurde , den .?Operator in C 6 / Visual Studio 2015 , .

Wir waren alle dort, wir haben einige tiefe Eigenschaften wie cake.frosting.berries.loader, die wir überprüfen müssen, ob sie null sind, damit es keine Ausnahme gibt. Der Weg, dies zu tun, besteht darin, eine kurzschließende if-Anweisung zu verwenden

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

Dies ist nicht gerade elegant, und es sollte möglicherweise eine einfachere Möglichkeit geben, die gesamte Kette zu überprüfen und festzustellen, ob sie auf eine Nullvariable / -eigenschaft stößt.

Ist es möglich, eine Erweiterungsmethode zu verwenden, oder wäre es eine Sprachfunktion, oder ist es nur eine schlechte Idee?


3
Ich habe mir das oft genug gewünscht - aber alle Ideen, die ich hatte, waren schlimmer als das eigentliche Problem.
Peterchen

Vielen Dank für alle Antworten und interessant zu sehen, dass andere Menschen die gleichen Gedanken hatten. Ich musste darüber nachdenken, wie ich das gerne selbst lösen würde, und obwohl Erics Lösungen nett sind, denke ich, ich würde einfach so etwas schreiben, wenn (IsNull (abc)) oder wenn (IsNotNull (abc)), aber vielleicht das ist nur nach meinem Geschmack :)
Homde

Wenn Sie das Zuckerguss instanziieren, hat es die Eigenschaft von Beeren. Können Sie also an diesem Punkt in Ihrem Konstruktor dem Zuckerguss einfach sagen, wann immer es veranlasst wird, leere (nicht null) Beeren zu erzeugen? und wann immer Beeren modifiziert Zuckerguss macht die Überprüfung des Wertes ????
Doug Chamberlain

Einige der Techniken, die ich hier als etwas lose verwandt empfand, waren für das Problem der "tiefen Nullen", das ich zu umgehen versuchte, vorzuziehen. stackoverflow.com/questions/818642/…
AaronLS

Antworten:


223

Wir haben überlegt, eine neue Operation "?" auf die Sprache, die die gewünschte Semantik hat. (Und es wurde jetzt hinzugefügt; siehe unten.) Das heißt, Sie würden sagen

cake?.frosting?.berries?.loader

und der Compiler würde alle Kurzschlussprüfungen für Sie generieren.

Es hat die Messlatte für C # 4 nicht erreicht. Vielleicht für eine hypothetische zukünftige Version der Sprache.

Update (2014): Der ?.Betreiber ist jetzt geplant für die nächste Roslyn Compiler - Release. Beachten Sie, dass es immer noch einige Debatten über die genaue syntaktische und semantische Analyse des Operators gibt.

Update (Juli 2015): Visual Studio 2015 wurde veröffentlicht und wird mit einem C # -Compiler geliefert, der die nullbedingten Operatoren ?.und unterstützt?[] .


10
Ohne den Punkt wird es mit dem bedingten Operator (A? B: C) syntaktisch mehrdeutig. Wir versuchen, lexikalische Konstrukte zu vermeiden, bei denen wir willkürlich weit im Token-Stream nach vorne schauen müssen. (Obwohl es leider bereits solche Konstrukte in C # gibt; wir möchten lieber keine weiteren hinzufügen.)
Eric Lippert

33
@ Ian: Dieses Problem ist sehr häufig. Dies ist eine der häufigsten Anfragen, die wir erhalten.
Eric Lippert

7
@Ian: Ich bevorzuge es auch, das Nullobjektmuster zu verwenden, wenn dies möglich ist, aber die meisten Menschen haben nicht den Luxus, mit Objektmodellen zu arbeiten, die sie selbst entworfen haben. Viele existierende Objektmodelle verwenden Nullen und das ist die Welt, mit der wir leben müssen.
Eric Lippert

12
@ John: Wir erhalten diese Feature-Anfrage fast ausschließlich von unseren erfahrensten Programmierern. Die MVPs fragen ständig danach . Aber ich verstehe, dass die Meinungen unterschiedlich sind; Wenn Sie zusätzlich zu Ihrer Kritik einen konstruktiven Vorschlag für ein Sprachdesign machen möchten, denke ich gerne darüber nach.
Eric Lippert

28
@lazyberezovsky: Ich habe das sogenannte "Gesetz" von Demeter nie verstanden; Zunächst scheint es genauer als "The Suggestion of Demeter" bezeichnet zu werden. Und zweitens ist das Ergebnis der Annahme "nur eines Mitglieds" zu seiner logischen Schlussfolgerung "Gott-Objekte", bei denen jedes Objekt alles für jeden Kunden tun muss, anstatt Objekte verteilen zu können, die wissen, wie man das tut, was der Kunde tut will. Ich bevorzuge das genaue Gegenteil des Demeter-Gesetzes: Jedes Objekt löst eine kleine Anzahl von Problemen gut, und eine dieser Lösungen kann sein: "Hier ist ein anderes Objekt, das Ihr Problem besser löst"
Eric Lippert

27

Diese Frage hat mich dazu inspiriert, herauszufinden, wie diese Art der tiefen Nullprüfung mit einer einfacheren / schöneren Syntax unter Verwendung von Ausdrucksbäumen durchgeführt werden kann. Ich stimme zwar den Antworten zu, die besagen, dass dies möglich ist ein schlechtes Design sein , wenn Sie häufig auf Instanzen tief in der Hierarchie zugreifen müssen, aber ich denke auch, dass es in einigen Fällen, wie z. B. bei der Datenpräsentation, sehr nützlich sein kann.

Also habe ich eine Erweiterungsmethode erstellt, mit der Sie schreiben können:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

Dies gibt die Beeren zurück, wenn kein Teil des Ausdrucks null ist. Wenn null angetroffen wird, wird null zurückgegeben. Es gibt jedoch einige Einschränkungen: In der aktuellen Version funktioniert es nur mit einfachem Mitgliederzugriff und nur mit .NET Framework 4, da die in Version 4 neue Methode MemberExpression.Update verwendet wird. Dies ist der Code für die IfNotNull-Erweiterungsmethode:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace dr.IfNotNullOperator.PoC
{
    public static class ObjectExtensions
    {
        public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            if (ReferenceEquals(arg, null))
                return default(TResult);

            var stack = new Stack<MemberExpression>();
            var expr = expression.Body as MemberExpression;
            while(expr != null)
            {
                stack.Push(expr);
                expr = expr.Expression as MemberExpression;
            } 

            if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression))
                throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.",
                                                             expression));

            object a = arg;
            while(stack.Count > 0)
            {
                expr = stack.Pop();
                var p = expr.Expression as ParameterExpression;
                if (p == null)
                {
                    p = Expression.Parameter(a.GetType(), "x");
                    expr = expr.Update(p);
                }
                var lambda = Expression.Lambda(expr, p);
                Delegate t = lambda.Compile();                
                a = t.DynamicInvoke(a);
                if (ReferenceEquals(a, null))
                    return default(TResult);
            }

            return (TResult)a;            
        }
    }
}

Es funktioniert, indem der Ausdrucksbaum, der Ihren Ausdruck darstellt, untersucht und die Teile nacheinander bewertet werden. jedes Mal, wenn überprüft wird, ob das Ergebnis nicht null ist.

Ich bin sicher, dass dies erweitert werden könnte, so dass andere Ausdrücke als MemberExpression unterstützt werden. Betrachten Sie dies als Proof-of-Concept-Code und denken Sie daran, dass die Verwendung einen Leistungsverlust zur Folge hat (was in vielen Fällen wahrscheinlich keine Rolle spielt, aber nicht in einer engen Schleife verwendet wird :-)).


Ich bin beeindruckt von Ihren Lambda-Fähigkeiten :) Die Syntax scheint jedoch etwas komplexer zu sein, als man es gerne hätte, zumindest für das If-Statement-Szenario
Homde

Cool, aber es läuft wie 100x mehr Code als ein if .. &&. Es lohnt sich nur, wenn es noch zu einem if .. && kompiliert wird.
Monstieur

1
Ah und dann habe ich DynamicInvokedort gesehen. Ich vermeide das religiös :)
Nawfal

24

Ich habe festgestellt, dass diese Erweiterung für Deep-Nesting-Szenarien sehr nützlich ist.

public static R Coal<T, R>(this T obj, Func<T, R> f)
    where T : class
{
    return obj != null ? f(obj) : default(R);
}

Diese Idee habe ich vom Null-Koaleszenz-Operator in C # und T-SQL abgeleitet. Das Schöne ist, dass der Rückgabetyp immer der Rückgabetyp der inneren Eigenschaft ist.

Auf diese Weise können Sie Folgendes tun:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

... oder eine geringfügige Abweichung von den oben genannten:

var berries = cake.Coal(x => x.frosting, x => x.berries);

Es ist nicht die beste Syntax, die ich kenne, aber es funktioniert.


Warum "Kohle", das sieht extrem gruselig aus. ;) Ihre Probe würde jedoch versagen, wenn der Zuckerguss null wäre. Hätte so aussehen sollen: var berries = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Robert Giesecke

Oh, aber Sie implizieren, dass das zweite Argument kein Aufruf an Coal ist, was es natürlich sein muss. Es ist nur eine bequeme Änderung. Der Selektor (x => x.berries) wird an einen Coal-Aufruf innerhalb der Coal-Methode übergeben, der zwei Argumente akzeptiert.
John Leidegren

Der Name Coalescing oder Coalesce wurde von T-SQL übernommen, da kam mir zuerst die Idee. IfNotNull impliziert, dass etwas stattfindet, wenn es nicht null ist. Was dies jedoch ist, wird durch den Methodenaufruf IfNotNull nicht erklärt. Kohle ist in der Tat ein seltsamer Name, aber dies ist in der Tat eine seltsame Methode, die es wert ist, beachtet zu werden.
John Leidegren

Der buchstäblich beste Name dafür wäre so etwas wie "ReturnIfNotNull" oder "ReturnOrDefault"
John Leidegren

@flq +1 ... in unserem Projekt heißt es auch IfNotNull :)
Marc Sigrist

16

Abgesehen von der Verletzung des Demeter-Gesetzes, wie Mehrdad Afshari bereits betont hat, scheint es mir, dass Sie für die Entscheidungslogik eine "tiefe Nullprüfung" benötigen.

Dies ist meistens der Fall, wenn Sie leere Objekte durch Standardwerte ersetzen möchten. In diesem Fall sollten Sie das Nullobjektmuster implementieren . Es fungiert als Ersatz für ein reales Objekt und bietet Standardwerte und "Nicht-Aktions" -Methoden.


Nein, Objective-C ermöglicht das Senden von Nachrichten an Nullobjekte und gibt bei Bedarf den entsprechenden Standardwert zurück. Keine Probleme da.
Johannes Rudolph

2
Ja. Das ist der Punkt. Grundsätzlich emulieren Sie das ObjC-Verhalten mit Null Object Pattern.
Mehrdad Afshari

10

Update: Ab Visual Studio 2015 erkennt der C # -Compiler (Sprachversion 6) jetzt den ?.Operator, was das "Deep Null Checking" zum Kinderspiel macht. Siehe diese Antwort für Details.

Abgesehen von der Neugestaltung Ihres Codes, wie in dieser gelöschten Antwort vorgeschlagen, besteht eine andere (wenn auch schreckliche) Option darin, einen try…catchBlock zu verwenden, um festzustellen, ob NullReferenceExceptionirgendwann während dieser tiefen Eigenschaftssuche ein Fehler auftritt.

try
{
    var x = cake.frosting.berries.loader;
    ...
}
catch (NullReferenceException ex)
{
    // either one of cake, frosting, or berries was null
    ...
}

Ich persönlich würde dies aus folgenden Gründen nicht tun:

  • Es sieht nicht gut aus.
  • Es verwendet die Ausnahmebehandlung, die auf Ausnahmesituationen abzielen sollte und nicht auf etwas, das Sie im normalen Betriebsverlauf häufig erwarten.
  • NullReferenceExceptions sollte wahrscheinlich nie explizit gefangen werden. (Siehe diese Frage .)

Ist es also möglich, eine Erweiterungsmethode zu verwenden, oder wäre es eine Sprachfunktion, [...]

Dies müsste mit ziemlicher Sicherheit eine Sprachfunktion sein (die in C # 6 in Form von .?und ?[]Operatoren verfügbar ist ), es sei denn, C # hatte bereits eine ausgefeiltere verzögerte Auswertung oder Sie möchten Reflektion verwenden (was wahrscheinlich auch keine ist gute Idee aus Gründen der Leistung und Typensicherheit).

Da es keine Möglichkeit gibt, einfach cake.frosting.berries.loaderan eine Funktion zu übergeben (sie würde ausgewertet und eine Nullreferenzausnahme auslösen), müssten Sie eine allgemeine Suchmethode folgendermaßen implementieren: Sie nimmt Objekte und die Namen von Eigenschaften auf Sieh nach oben:

static object LookupProperty( object startingPoint, params string[] lookupChain )
{
    // 1. if 'startingPoint' is null, return null, or throw an exception.
    // 2. recursively look up one property/field after the other from 'lookupChain',
    //    using reflection.
    // 3. if one lookup is not possible, return null, or throw an exception.
    // 3. return the last property/field's value.
}

...

var x = LookupProperty( cake, "frosting", "berries", "loader" );

(Hinweis: Code bearbeitet.)

Sie sehen schnell mehrere Probleme mit einem solchen Ansatz. Erstens erhalten Sie keine Typensicherheit und kein mögliches Boxen von Eigenschaftswerten eines einfachen Typs. Zweitens können Sie entweder zurückkehren, nullwenn etwas schief geht, und Sie müssen dies in Ihrer aufrufenden Funktion überprüfen, oder Sie lösen eine Ausnahme aus, und Sie kehren zu Ihrem Ausgangspunkt zurück. Drittens könnte es langsam sein. Viertens sieht es hässlicher aus als das, womit Sie begonnen haben.

[...] oder ist es nur eine schlechte Idee?

Ich würde entweder bleiben bei:

if (cake != null && cake.frosting != null && ...) ...

oder gehen Sie mit der obigen Antwort von Mehrdad Afshari.


PS: Als ich diese Antwort schrieb, habe ich offensichtlich keine Ausdrucksbäume für Lambda-Funktionen berücksichtigt. Siehe zB die Antwort von @driis für eine Lösung in dieser Richtung. Es basiert auch auf einer Art Reflexion und ist daher möglicherweise nicht ganz so gut wie eine einfachere Lösung ( if (… != null & … != null) …), kann jedoch aus syntaktischer Sicht besser beurteilt werden.


2
Ich weiß nicht, warum dies abgelehnt wurde, ich habe eine Gegenstimme für das Gleichgewicht abgegeben: Die Antwort ist richtig und bringt einen neuen Aspekt mit sich (und erwähnt explizit die Nachteile dieser Lösung ...)
MartinStettner

Wo ist "die obige Antwort von Mehrdad Afshari"?
Marson Mao

1
@MarsonMao: Diese Antwort wurde inzwischen gelöscht. (Sie können es immer noch lesen, wenn Ihr SO-Rang ausreichend hoch ist.) Vielen Dank, dass Sie auf meinen Fehler hingewiesen haben: Ich sollte über einen Hyperlink auf andere Antworten verweisen und keine Wörter wie "siehe oben" / "siehe unten" (seitdem) verwenden Antworten erscheinen nicht in einer festen Reihenfolge). Ich habe meine Antwort aktualisiert.
stakx - nicht mehr

5

Die Antwort von driis ist zwar interessant, aber meiner Meinung nach in Bezug auf die Leistung etwas zu teuer. Anstatt viele Delegaten zu kompilieren, würde ich es vorziehen, ein Lambda pro Eigenschaftspfad zu kompilieren, es zwischenzuspeichern und dann viele Typen neu aufzurufen.

NullCoalesce unten macht genau das. Es gibt einen neuen Lambda-Ausdruck mit Nullprüfungen und einer Standardrückgabe (TResult) zurück, falls ein Pfad null ist.

Beispiel:

NullCoalesce((Process p) => p.StartInfo.FileName)

Gibt einen Ausdruck zurück

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

Code:

    static void Main(string[] args)
    {
        var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked);
        var converted2 = NullCoalesce((string[] s) => s.Length);
    }

    private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
    {
        var test = GetTest(lambdaExpression.Body);
        if (test != null)
        {
            return Expression.Lambda<Func<TSource, TResult>>(
                Expression.Condition(
                    test,
                    lambdaExpression.Body,
                    Expression.Default(
                        typeof(TResult)
                    )
                ),
                lambdaExpression.Parameters
            );
        }
        return lambdaExpression;
    }

    private static Expression GetTest(Expression expression)
    {
        Expression container;
        switch (expression.NodeType)
        {
            case ExpressionType.ArrayLength:
                container = ((UnaryExpression)expression).Operand;
                break;
            case ExpressionType.MemberAccess:
                if ((container = ((MemberExpression)expression).Expression) == null)
                {
                    return null;
                }
                break;
            default:
                return null;
        }
        var baseTest = GetTest(container);
        if (!container.Type.IsValueType)
        {
            var containerNotNull = Expression.NotEqual(
                container,
                Expression.Default(
                    container.Type
                )
            );
            return (baseTest == null ?
                containerNotNull :
                Expression.AndAlso(
                    baseTest,
                    containerNotNull
                )
            );
        }
        return baseTest;
    }


3

Auch ich habe mir oft eine einfachere Syntax gewünscht! Es wird besonders hässlich, wenn Sie Methodenrückgabewerte haben, die möglicherweise null sind, weil Sie dann zusätzliche Variablen benötigen (zum Beispiel:cake.frosting.flavors.FirstOrDefault().loader )

Hier ist jedoch eine ziemlich anständige Alternative, die ich verwende: Erstellen Sie eine Null-Safe-Chain-Hilfsmethode. Mir ist klar, dass dies der obigen Antwort von @ John (mit der CoalErweiterungsmethode) ziemlich ähnlich ist, aber ich finde, dass es einfacher ist und weniger tippt. So sieht es aus:

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

Hier ist die Implementierung:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r) 
where TA:class where TB:class where TC:class {
    if (a == null) return default(TResult);
    var B = b(a);
    if (B == null) return default(TResult);
    var C = c(B);
    if (C == null) return default(TResult);
    return r(C);
}

Ich habe auch mehrere Überladungen (mit 2 bis 6 Parametern) sowie Überladungen erstellt, die es der Kette ermöglichen, mit einem Werttyp oder einer Standardeinstellung zu enden. Das funktioniert wirklich gut für mich!



1

Wie in der Antwort von John Leidegren vorgeschlagen , besteht ein Ansatz zur Umgehung dieses Problems darin , Erweiterungsmethoden und Delegaten zu verwenden. Ihre Verwendung könnte ungefähr so ​​aussehen:

int? numberOfBerries = cake
    .NullOr(c => c.Frosting)
    .NullOr(f => f.Berries)
    .NullOr(b => b.Count());

Die Implementierung ist unübersichtlich, da sie für Werttypen, Referenztypen und nullfähige Werttypen funktionieren muss. Sie können eine vollständige Implementierung in finden Timwi ‚s Antwort auf Was ist der richtige Weg ist für Null - Werte zu überprüfen? .


1

Oder Sie können Reflexion verwenden :)

Reflexionsfunktion:

public Object GetPropValue(String name, Object obj)
    {
        foreach (String part in name.Split('.'))
        {
            if (obj == null) { return null; }

            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }

Verwendung:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

Mein Fall (DBNull.Value anstelle von null in der Reflexionsfunktion zurückgeben):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));

1

Versuchen Sie diesen Code:

    /// <summary>
    /// check deep property
    /// </summary>
    /// <param name="obj">instance</param>
    /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
    /// <returns>if null return true else return false</returns>
    public static bool IsNull(this object obj, string property)
    {
        if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty");
        if (obj != null)
        {
            string[] deep = property.Split('.');
            object instance = obj;
            Type objType = instance.GetType();
            PropertyInfo propertyInfo;
            foreach (string p in deep)
            {
                propertyInfo = objType.GetProperty(p);
                if (propertyInfo == null) throw new Exception("No property : " + p);
                instance = propertyInfo.GetValue(instance, null);
                if (instance != null)
                    objType = instance.GetType();
                else
                    return true;
            }
            return false;
        }
        else
            return true;
    }

0

Ich habe das letzte Nacht gepostet und dann hat mich ein Freund auf diese Frage hingewiesen. Ich hoffe es hilft. Sie können dann so etwas tun:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue");


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DeepNullCoalescence
{
  public static class Dis
  {
    public static T OrDat<T>(Expression<Func><T>> expr, T dat)
    {
      try
      {
        var func = expr.Compile();
        var result = func.Invoke();
        return result ?? dat; //now we can coalesce
      }
      catch (NullReferenceException)
      {
        return dat;
      }
    }
  }
}

Lesen Sie hier den vollständigen Blog-Beitrag .

Der gleiche Freund schlug auch vor, dass Sie dies sehen .


3
Warum sich mit einem beschäftigen, Expressionwenn Sie nur kompilieren und fangen wollen? Verwenden Sie einfach eine Func<T>.
Scott Rippey

0

Ich habe den Code von hier aus leicht geändert , damit er für die gestellte Frage funktioniert:

public static class GetValueOrDefaultExtension
{
    public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector)
    {
        try { return selector(source); }
        catch { return default(TResult); }
    }
}

Und ja, dies ist wahrscheinlich nicht die optimale Lösung, da die Leistung beeinträchtigt wird, aber es funktioniert:>

Verwendung:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);

0

Wo Sie dies erreichen müssen, tun Sie Folgendes:

Verwendung

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

oder

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

Implementierung der Hilfsklasse

public static class Complex
{
    public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
    {
        return Get(() => func(root));
    }

    public static T Get<T>(Func<T> func)
    {
        try
        {
            return func();
        }
        catch (Exception)
        {
            return default(T);
        }
    }
}

-3

Ich mag den Ansatz von Objective-C:

"Die Objective-C-Sprache verfolgt einen anderen Ansatz für dieses Problem und ruft keine Methoden für nil auf, sondern gibt für alle derartigen Aufrufe nil zurück."

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}

1
Was eine andere Sprache tut (und Ihre Meinung dazu), ist fast völlig irrelevant, damit sie in C # funktioniert. Es hilft niemandem, sein C # -Problem zu lösen
ADyson
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.