Wie kann ich ein aktuelles Element aus HashSet <T> abrufen?


83

Ich habe diese Frage gelesen , warum dies nicht möglich ist, aber keine Lösung für das Problem gefunden.

Ich möchte ein Element aus einem .NET abrufen HashSet<T>. Ich suche nach einer Methode, die diese Signatur haben würde:

/// <summary>
/// Determines if this set contains an item equal to <paramref name="item"/>, 
/// according to the comparison mechanism that was used when the set was created. 
/// The set is not changed. If the set does contain an item equal to 
/// <paramref name="item"/>, then the item from the set is returned.
/// </summary>
bool TryGetItem<T>(T item, out T foundItem);

Das Durchsuchen des Satzes nach einem Gegenstand mit einer solchen Methode wäre O (1). Die einzige Möglichkeit, ein Element von a abzurufen, HashSet<T>besteht darin, alle Elemente aufzulisten, die O (n) sind.

Ich habe keine andere Problemumgehung für dieses Problem gefunden, als meine eigene zu erstellen HashSet<T>oder eine zu verwenden Dictionary<K, V>. Irgendeine andere Idee?

Hinweis:
Ich möchte nicht überprüfen, ob HashSet<T>der Artikel enthalten ist. Ich möchte den Verweis auf das Element erhalten, das in der gespeichert ist, HashSet<T>da ich es aktualisieren muss (ohne es durch eine andere Instanz zu ersetzen). Das Element, an das ich übergeben würde, TryGetItemwäre gleich (gemäß dem Vergleichsmechanismus, den ich an den Konstruktor übergeben habe), aber es wäre nicht dieselbe Referenz.


1
Verwenden Sie Contains und geben Sie das übergebene Element als Eingabe zurück.
Mathias


2
Wenn Sie ein Objekt anhand eines Schlüsselwerts nachschlagen müssen, ist Dictionary <T> möglicherweise die geeignetere Sammlung, um es zu speichern.
ThatBlairGuy

@ThatBlairGuy: Du hast recht. Ich denke, ich werde meine eigene Set-Sammlung mithilfe eines Wörterbuchs intern implementieren, um meine Artikel zu speichern. Der Schlüssel ist der HashCode des Elements. Ich habe ungefähr die gleiche Leistung wie ein HashSet und muss nicht jedes Mal einen Schlüssel angeben, wenn ich einen Artikel aus meiner Sammlung hinzufügen / entfernen / abrufen muss.
Francois C

2
@mathias Da das Hashset möglicherweise ein Element enthält, das der Eingabe entspricht, aber nicht identisch ist. Beispielsweise möchten Sie möglicherweise ein Hashset von Referenztypen haben, aber Sie möchten den Inhalt vergleichen, nicht die Referenz für Gleichheit.
NounVerber

Antworten:


24

Was Sie verlangen, wurde vor einem Jahr zu .NET Core und kürzlich zu .NET 4.7.2 hinzugefügt :

In .NET Framework 4.7.2 haben wir den Standard-Sammlungstypen einige APIs hinzugefügt, die neue Funktionen wie folgt ermöglichen.
- 'TryGetValue' wird zu SortedSet und HashSet hinzugefügt, um dem in anderen Sammlungstypen verwendeten Try-Muster zu entsprechen.

Die Signatur lautet wie folgt (in .NET 4.7.2 und höher):

    //
    // Summary:
    //     Searches the set for a given value and returns the equal value it finds, if any.
    //
    // Parameters:
    //   equalValue:
    //     The value to search for.
    //
    //   actualValue:
    //     The value from the set that the search found, or the default value of T when
    //     the search yielded no match.
    //
    // Returns:
    //     A value indicating whether the search was successful.
    public bool TryGetValue(T equalValue, out T actualValue);

PS .: Falls Sie interessiert sind, gibt es verwandte Funktionen, die sie in Zukunft hinzufügen - HashSet.GetOrAdd (T).


65

Dies ist tatsächlich eine große Lücke in der Sammlung. Sie benötigen entweder nur ein Schlüsselwörterbuch oder ein HashSet, mit dem Objektreferenzen abgerufen werden können. So viele Leute haben danach gefragt, warum es nicht repariert wird, ist mir ein Rätsel.

Ohne Bibliotheken von Drittanbietern besteht die beste Problemumgehung darin, Dictionary<T, T>Schlüssel zu verwenden, die mit Werten identisch sind, da Dictionary seine Einträge als Hash-Tabelle speichert. In Bezug auf die Leistung ist es dasselbe wie das HashSet, aber es verschwendet natürlich Speicher (Größe eines Zeigers pro Eintrag).

Dictionary<T, T> myHashedCollection;
...
if(myHashedCollection.ContainsKey[item])
    item = myHashedCollection[item]; //replace duplicate
else
    myHashedCollection.Add(item, item); //add previously unknown item
...
//work with unique item

1
Ich würde vorschlagen, dass die Schlüssel zu seinem Wörterbuch das sein sollten, was er derzeit in seinem EqualityComparer für das Hashset platziert hat. Ich finde es schmutzig, einen EqualityComparer zu verwenden, wenn Sie nicht wirklich sagen, dass die Elemente gleich sind (andernfalls könnten Sie einfach das Element verwenden, das Sie für den Vergleich erstellt haben). Ich würde eine Klasse / Struktur erstellen, die den Schlüssel darstellt. Dies geht natürlich zu Lasten von mehr Speicher.
Ed T

1
Da der Schlüssel in Value gespeichert ist, empfehle ich, anstelle von Dictionary eine von KeyedCollection geerbte Sammlung zu verwenden. msdn.microsoft.com/en-us/library/ms132438(v=vs.110).aspx
Zugriff verweigert

11

Diese Methode wurde zu .NET Framework 4.7.2 (und zuvor zu .NET Core 2.0 ) hinzugefügt . siehe HashSet<T>.TryGetValue. Zitieren der Quelle :

/// <summary>
/// Searches the set for a given value and returns the equal value it finds, if any.
/// </summary>
/// <param name="equalValue">The value to search for.
/// </param>
/// <param name="actualValue">
/// The value from the set that the search found, or the default value
/// of <typeparamref name="T"/> when the search yielded no match.</param>
/// <returns>A value indicating whether the search was successful.</returns>
/// <remarks>
/// This can be useful when you want to reuse a previously stored reference instead of 
/// a newly constructed one (so that more sharing of references can occur) or to look up
/// a value that has more complete data than the value you currently have, although their
/// comparer functions indicate they are equal.
/// </remarks>
public bool TryGetValue(T equalValue, out T actualValue)

1
Sowie auch für SortedSet .
Nawfal

4

Was ist mit der Überladung des String-Gleichheitsvergleichs:

  class StringEqualityComparer : IEqualityComparer<String>
{
    public string val1;
    public bool Equals(String s1, String s2)
    {
        if (!s1.Equals(s2)) return false;
        val1 = s1;
        return true;
    }

    public int GetHashCode(String s)
    {
        return s.GetHashCode();
    }
}
public static class HashSetExtension
{
    public static bool TryGetValue(this HashSet<string> hs, string value, out string valout)
    {
        if (hs.Contains(value))
        {
            valout=(hs.Comparer as StringEqualityComparer).val1;
            return true;
        }
        else
        {
            valout = null;
            return false;
        }
    }
}

Und deklarieren Sie dann das HashSet als:

HashSet<string> hs = new HashSet<string>(new StringEqualityComparer());

Hier geht es um die Speicherverwaltung - die Rückgabe des tatsächlichen Elements im Hashset anstelle einer identischen Kopie. Im obigen Code finden wir also die Zeichenfolge mit demselben Inhalt und geben dann einen Verweis darauf zurück. Für Strings ist dies ähnlich wie beim Internieren.
mp666

@zumalifeguard @ mp666 Es wird nicht garantiert, dass dies so funktioniert, wie es ist. Es würde jemanden erfordern, der das instanziiert HashSet, um den spezifischen Wertekonverter bereitzustellen. Eine optimale Lösung wäre, TryGetValueeine neue Instanz des Spezialisten zu übergeben StringEqualityComparer(andernfalls as StringEqualityComparerkönnte dies zu einer Null führen, wodurch der .val1Zugriff auf die Eigenschaft ausgelöst wird). Auf diese Weise kann StringEqualityComparer zu einer verschachtelten privaten Klasse in HashSetExtension werden. Im Falle eines überschriebenen Gleichheitsvergleichs sollte der StringEqualityComparer die Standardeinstellung aufrufen.
Graeme Wicksted

Sie müssen Ihr HashSet wie folgt deklarieren: HashSet <string> valueCash = new HashSet <string> (neuer StringEqualityComparer ())
mp666

1
Schmutziger Hack. Ich weiß, wie es funktioniert, aber es ist faul, damit es funktioniert
M.kazem Akhgary

3

Ein weiterer Trick würde Reflection InternalIndexOfausführen , indem auf die interne Funktion von HashSet zugegriffen wird. Beachten Sie, dass die Feldnamen fest codiert sind. Wenn sich diese in zukünftigen .NET-Versionen ändern, wird dies nicht funktionieren.

Hinweis: Wenn Sie Mono verwenden, sollten Sie den Feldnamen von m_slotsin ändern _slots.

internal static class HashSetExtensions<T>
{
    public delegate bool GetValue(HashSet<T> source, T equalValue, out T actualValue);

    public static GetValue TryGetValue { get; }

    static HashSetExtensions() {
        var targetExp = Expression.Parameter(typeof(HashSet<T>), "target");
        var itemExp   = Expression.Parameter(typeof(T), "item");
        var actualValueExp = Expression.Parameter(typeof(T).MakeByRefType(), "actualValueExp");

        var indexVar = Expression.Variable(typeof(int), "index");
        // ReSharper disable once AssignNullToNotNullAttribute
        var indexExp = Expression.Call(targetExp, typeof(HashSet<T>).GetMethod("InternalIndexOf", BindingFlags.NonPublic | BindingFlags.Instance), itemExp);

        var truePart = Expression.Block(
            Expression.Assign(
                actualValueExp, Expression.Field(
                    Expression.ArrayAccess(
                        // ReSharper disable once AssignNullToNotNullAttribute
                        Expression.Field(targetExp, typeof(HashSet<T>).GetField("m_slots", BindingFlags.NonPublic | BindingFlags.Instance)), indexVar),
                    "value")),
            Expression.Constant(true));

        var falsePart = Expression.Constant(false);

        var block = Expression.Block(
            new[] { indexVar },
            Expression.Assign(indexVar, indexExp),
            Expression.Condition(
                Expression.GreaterThanOrEqual(indexVar, Expression.Constant(0)),
                truePart,
                falsePart));

        TryGetValue = Expression.Lambda<GetValue>(block, targetExp, itemExp, actualValueExp).Compile();
    }
}

public static class Extensions
{
    public static bool TryGetValue2<T>(this HashSet<T> source, T equalValue,  out T actualValue) {
        if (source.Count > 0) {
            if (HashSetExtensions<T>.TryGetValue(source, equalValue, out actualValue)) {
                return true;
            }
        }
        actualValue = default;
        return false;
    }
}

Prüfung:

var x = new HashSet<int> { 1, 2, 3 };
if (x.TryGetValue2(1, out var value)) {
    Console.WriteLine(value);
}

2

Ok, also kannst du es so machen

YourObject x = yourHashSet.Where(w => w.Name.Contains("strin")).FirstOrDefault();

Dies dient zum Abrufen einer neuen Instanz des ausgewählten Objekts. Um Ihr Objekt zu aktualisieren, sollten Sie Folgendes verwenden:

yourHashSet.Where(w => w.Name.Contains("strin")).FirstOrDefault().MyProperty = "something";

Dies ist eine interessante Möglichkeit. Sie müssen nur die zweite in einen Versuch einschließen. Wenn Sie also nach etwas suchen, das nicht in der Liste enthalten ist, erhalten Sie eine NullReferenceExpection. Aber es ist ein Schritt in die richtige Richtung?
Piotr Kula

11
LINQ durchläuft die Sammlung in einer foreach-Schleife, dh O (n) Suchzeit. Es ist zwar eine Lösung für das Problem, aber es macht den Zweck der Verwendung eines HashSets in erster Linie zunichte.
Niklas Ekman


1

SortedSet hätte unter diesen Umständen wahrscheinlich eine Suchzeit von O (log n), wenn dies eine Option ist. Immer noch nicht O (1), aber zumindest besser.


1

Die Implementierung von @ mp666 answer wurde so geändert, dass sie für jede Art von HashSet verwendet werden kann und das Überschreiben des Standardgleichheitsvergleichs ermöglicht.

public interface IRetainingComparer<T> : IEqualityComparer<T>
{
    T Key { get; }
    void ClearKeyCache();
}

/// <summary>
/// An <see cref="IEqualityComparer{T}"/> that retains the last key that successfully passed <see cref="IEqualityComparer{T}.Equals(T,T)"/>.
/// This class relies on the fact that <see cref="HashSet{T}"/> calls the <see cref="IEqualityComparer{T}.Equals(T,T)"/> with the first parameter
/// being an existing element and the second parameter being the one passed to the initiating call to <see cref="HashSet{T}"/> (eg. <see cref="HashSet{T}.Contains(T)"/>).
/// </summary>
/// <typeparam name="T">The type of object being compared.</typeparam>
/// <remarks>This class is thread-safe but may should not be used with any sort of parallel access (PLINQ).</remarks>
public class RetainingEqualityComparerObject<T> : IRetainingComparer<T> where T : class
{
    private readonly IEqualityComparer<T> _comparer;

    [ThreadStatic]
    private static WeakReference<T> _retained;

    public RetainingEqualityComparerObject(IEqualityComparer<T> comparer)
    {
        _comparer = comparer;
    }

    /// <summary>
    /// The retained instance on side 'a' of the <see cref="Equals"/> call which successfully met the equality requirement agains side 'b'.
    /// </summary>
    /// <remarks>Uses a <see cref="WeakReference{T}"/> so unintended memory leaks are not encountered.</remarks>
    public T Key
    {
        get
        {
            T retained;
            return _retained == null ? null : _retained.TryGetTarget(out retained) ? retained : null;
        }
    }


    /// <summary>
    /// Sets the retained <see cref="Key"/> to the default value.
    /// </summary>
    /// <remarks>This should be called prior to performing an operation that calls <see cref="Equals"/>.</remarks>
    public void ClearKeyCache()
    {
        _retained = _retained ?? new WeakReference<T>(null);
        _retained.SetTarget(null);
    }

    /// <summary>
    /// Test two objects of type <see cref="T"/> for equality retaining the object if successful.
    /// </summary>
    /// <param name="a">An instance of <see cref="T"/>.</param>
    /// <param name="b">A second instance of <see cref="T"/> to compare against <paramref name="a"/>.</param>
    /// <returns>True if <paramref name="a"/> and <paramref name="b"/> are equal, false otherwise.</returns>
    public bool Equals(T a, T b)
    {
        if (!_comparer.Equals(a, b))
        {
            return false;
        }

        _retained = _retained ?? new WeakReference<T>(null);
        _retained.SetTarget(a);
        return true;
    }

    /// <summary>
    /// Gets the hash code value of an instance of <see cref="T"/>.
    /// </summary>
    /// <param name="o">The instance of <see cref="T"/> to obtain a hash code from.</param>
    /// <returns>The hash code value from <paramref name="o"/>.</returns>
    public int GetHashCode(T o)
    {
        return _comparer.GetHashCode(o);
    }
}

/// <summary>
/// An <see cref="IEqualityComparer{T}"/> that retains the last key that successfully passed <see cref="IEqualityComparer{T}.Equals(T,T)"/>.
/// This class relies on the fact that <see cref="HashSet{T}"/> calls the <see cref="IEqualityComparer{T}.Equals(T,T)"/> with the first parameter
/// being an existing element and the second parameter being the one passed to the initiating call to <see cref="HashSet{T}"/> (eg. <see cref="HashSet{T}.Contains(T)"/>).
/// </summary>
/// <typeparam name="T">The type of object being compared.</typeparam>
/// <remarks>This class is thread-safe but may should not be used with any sort of parallel access (PLINQ).</remarks>
public class RetainingEqualityComparerStruct<T> : IRetainingComparer<T> where T : struct 
{
    private readonly IEqualityComparer<T> _comparer;

    [ThreadStatic]
    private static T _retained;

    public RetainingEqualityComparerStruct(IEqualityComparer<T> comparer)
    {
        _comparer = comparer;
    }

    /// <summary>
    /// The retained instance on side 'a' of the <see cref="Equals"/> call which successfully met the equality requirement agains side 'b'.
    /// </summary>
    public T Key => _retained;


    /// <summary>
    /// Sets the retained <see cref="Key"/> to the default value.
    /// </summary>
    /// <remarks>This should be called prior to performing an operation that calls <see cref="Equals"/>.</remarks>
    public void ClearKeyCache()
    {
        _retained = default(T);
    }

    /// <summary>
    /// Test two objects of type <see cref="T"/> for equality retaining the object if successful.
    /// </summary>
    /// <param name="a">An instance of <see cref="T"/>.</param>
    /// <param name="b">A second instance of <see cref="T"/> to compare against <paramref name="a"/>.</param>
    /// <returns>True if <paramref name="a"/> and <paramref name="b"/> are equal, false otherwise.</returns>
    public bool Equals(T a, T b)
    {
        if (!_comparer.Equals(a, b))
        {
            return false;
        }

        _retained = a;
        return true;
    }

    /// <summary>
    /// Gets the hash code value of an instance of <see cref="T"/>.
    /// </summary>
    /// <param name="o">The instance of <see cref="T"/> to obtain a hash code from.</param>
    /// <returns>The hash code value from <paramref name="o"/>.</returns>
    public int GetHashCode(T o)
    {
        return _comparer.GetHashCode(o);
    }
}

/// <summary>
/// Provides TryGetValue{T} functionality similar to that of <see cref="IDictionary{TKey,TValue}"/>'s implementation.
/// </summary>
public class ExtendedHashSet<T> : HashSet<T>
{
    /// <summary>
    /// This class is guaranteed to wrap the <see cref="IEqualityComparer{T}"/> with one of the <see cref="IRetainingComparer{T}"/>
    /// implementations so this property gives convenient access to the interfaced comparer.
    /// </summary>
    private IRetainingComparer<T> RetainingComparer => (IRetainingComparer<T>)Comparer;

    /// <summary>
    /// Creates either a <see cref="RetainingEqualityComparerStruct{T}"/> or <see cref="RetainingEqualityComparerObject{T}"/>
    /// depending on if <see cref="T"/> is a reference type or a value type.
    /// </summary>
    /// <param name="comparer">(optional) The <see cref="IEqualityComparer{T}"/> to wrap. This will be set to <see cref="EqualityComparer{T}.Default"/> if none provided.</param>
    /// <returns>An instance of <see cref="IRetainingComparer{T}"/>.</returns>
    private static IRetainingComparer<T> Create(IEqualityComparer<T> comparer = null)
    {
        return (IRetainingComparer<T>) (typeof(T).IsValueType ? 
            Activator.CreateInstance(typeof(RetainingEqualityComparerStruct<>)
                .MakeGenericType(typeof(T)), comparer ?? EqualityComparer<T>.Default)
            :
            Activator.CreateInstance(typeof(RetainingEqualityComparerObject<>)
                .MakeGenericType(typeof(T)), comparer ?? EqualityComparer<T>.Default));
    }

    public ExtendedHashSet() : base(Create())
    {
    }

    public ExtendedHashSet(IEqualityComparer<T> comparer) : base(Create(comparer))
    {
    }

    public ExtendedHashSet(IEnumerable<T> collection) : base(collection, Create())
    {
    }

    public ExtendedHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer) : base(collection, Create(comparer))
    {
    }

    /// <summary>
    /// Attempts to find a key in the <see cref="HashSet{T}"/> and, if found, places the instance in <paramref name="original"/>.
    /// </summary>
    /// <param name="value">The key used to search the <see cref="HashSet{T}"/>.</param>
    /// <param name="original">
    /// The matched instance from the <see cref="HashSet{T}"/> which is not neccessarily the same as <paramref name="value"/>.
    /// This will be set to null for reference types or default(T) for value types when no match found.
    /// </param>
    /// <returns>True if a key in the <see cref="HashSet{T}"/> matched <paramref name="value"/>, False if no match found.</returns>
    public bool TryGetValue(T value, out T original)
    {
        var comparer = RetainingComparer;
        comparer.ClearKeyCache();

        if (Contains(value))
        {
            original = comparer.Key;
            return true;
        }

        original = default(T);
        return false;
    }
}

public static class HashSetExtensions
{
    /// <summary>
    /// Attempts to find a key in the <see cref="HashSet{T}"/> and, if found, places the instance in <paramref name="original"/>.
    /// </summary>
    /// <param name="hashSet">The instance of <see cref="HashSet{T}"/> extended.</param>
    /// <param name="value">The key used to search the <see cref="HashSet{T}"/>.</param>
    /// <param name="original">
    /// The matched instance from the <see cref="HashSet{T}"/> which is not neccessarily the same as <paramref name="value"/>.
    /// This will be set to null for reference types or default(T) for value types when no match found.
    /// </param>
    /// <returns>True if a key in the <see cref="HashSet{T}"/> matched <paramref name="value"/>, False if no match found.</returns>
    /// <exception cref="ArgumentNullException">If <paramref name="hashSet"/> is null.</exception>
    /// <exception cref="ArgumentException">
    /// If <paramref name="hashSet"/> does not have a <see cref="HashSet{T}.Comparer"/> of type <see cref="IRetainingComparer{T}"/>.
    /// </exception>
    public static bool TryGetValue<T>(this HashSet<T> hashSet, T value, out T original)
    {
        if (hashSet == null)
        {
            throw new ArgumentNullException(nameof(hashSet));
        }

        if (hashSet.Comparer.GetType().IsInstanceOfType(typeof(IRetainingComparer<T>)))
        {
            throw new ArgumentException($"HashSet must have an equality comparer of type '{nameof(IRetainingComparer<T>)}' to use this functionality", nameof(hashSet));
        }

        var comparer = (IRetainingComparer<T>)hashSet.Comparer;
        comparer.ClearKeyCache();

        if (hashSet.Contains(value))
        {
            original = comparer.Key;
            return true;
        }

        original = default(T);
        return false;
    }
}

1
Da Sie die Linq-Erweiterungsmethode verwenden Enumerable.Contains, werden alle Elemente des Satzes aufgelistet und verglichen, wodurch alle Vorteile verloren gehen, die die Hash-Implementierung des Satzes bietet. Dann können Sie auch einfach schreiben set.SingleOrDefault(e => set.Comparer.Equals(e, obj)), das das gleiche Verhalten und die gleichen Leistungsmerkmale wie Ihre Lösung aufweist.
Daniel AA Pelsmaeker

@Virtlink Guter Fang - Du hast absolut Recht. Ich werde meine Antwort ändern.
Graeme Wicksted

Wenn Sie jedoch ein HashSet umbrechen würden, das Ihren Komparator intern verwendet, würde dies funktionieren. So: Utillib / ExtHashSet
Daniel AA Pelsmaeker

@Virtlink danke! Am Ende habe ich HashSet als eine Option verpackt, aber die Vergleicher und eine Erweiterungsmethode für zusätzliche Vielseitigkeit bereitgestellt. Es ist jetzt threadsicher und verliert keinen Speicher ... aber es ist viel mehr Code als ich gehofft hatte!
Graeme Wicksted

@Francois Das Schreiben des obigen Codes war eher eine Übung, um eine "optimale" Zeit- / Speicherlösung herauszufinden; Ich schlage jedoch nicht vor, dass Sie sich für diese Methode entscheiden. Die Verwendung eines Wörterbuchs <T, T> mit einem benutzerdefinierten IEqualityComparer ist viel einfacher und zukunftssicherer!
Graeme Wicksted

-2

HashSet verfügt über eine Contains (T) -Methode.

Sie können einen IEqualityComparer angeben, wenn Sie eine benutzerdefinierte Vergleichsmethode benötigen (z. B. ein Personenobjekt speichern, aber die SSN für den Gleichheitsvergleich verwenden).


-11

Sie können auch die ToList () -Methode verwenden und einen Indexer darauf anwenden.

HashSet<string> mySet = new HashSet();
mySet.Add("mykey");
string key = mySet.toList()[0];

Ich bin mir nicht sicher, warum Sie Stimmen verloren haben, als ich diese Logik angewendet habe, die funktioniert hat. Ich musste Werte aus einer Struktur extrahieren, die mit Dictionary <string, ISet <String >> begann, wobei das ISet x Werte enthielt. Der direkteste Weg, um diese Werte zu erhalten, bestand darin, das Wörterbuch zu durchlaufen und den Schlüssel und den ISet-Wert zu ziehen. Dann habe ich das ISet durchlaufen, um die einzelnen Werte anzuzeigen. Es ist nicht elegant, aber es hat funktioniert.
Rumpf
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.