Wie nehme ich mit LINQ alle Elemente bis auf das letzte in einer Sequenz?


130

Nehmen wir an, ich habe eine Sequenz.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();
// sequence now contains: 0,1,2,3,...,999999,1000000

Das Abrufen der Sequenz ist nicht billig und wird dynamisch generiert, und ich möchte sie nur einmal durchlaufen.

Ich möchte 0 - 999999 erhalten (dh alles außer dem letzten Element)

Ich erkenne, dass ich so etwas tun könnte:

sequence.Take(sequence.Count() - 1);

Dies führt jedoch zu zwei Aufzählungen über die große Sequenz.

Gibt es ein LINQ-Konstrukt, mit dem ich Folgendes tun kann:

sequence.TakeAllButTheLastElement();

3
Sie müssen zwischen einem O (2n) -Zeit- oder einem O (count) -Raumeffizienzalgorithmus wählen, bei dem letzterer auch Elemente in einem internen Array verschieben muss.
Dykam

1
Dario, würdest du das bitte jemandem erklären, der nicht so in großer Notation ist?
Alexn


Am Ende habe ich es zwischengespeichert, indem ich die Sammlung in List konvertiert und dann aufgerufen habe sequenceList.RemoveAt(sequence.Count - 1);. In meinem Fall ist es akzeptabel, weil ich es nach allen LINQ-Manipulationen in ein Array oder IReadOnlyCollectionsowieso konvertieren muss. Ich frage mich, was ist Ihr Anwendungsfall, bei dem Sie nicht einmal über Caching nachdenken? Wie ich sehen kann, führt selbst eine genehmigte Antwort ein Caching durch, so dass eine einfache Konvertierung Listmeiner Meinung nach viel einfacher und kürzer ist.
Pavels Ahmadulins

Antworten:


64

Ich kenne keine Linq-Lösung - aber Sie können den Algorithmus einfach selbst mit Generatoren codieren (Yield Return).

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source) {
    var it = source.GetEnumerator();
    bool hasRemainingItems = false;
    bool isFirst = true;
    T item = default(T);

    do {
        hasRemainingItems = it.MoveNext();
        if (hasRemainingItems) {
            if (!isFirst) yield return item;
            item = it.Current;
            isFirst = false;
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 10);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.TakeAllButLast().Select(x => x.ToString()).ToArray()));
}

Oder als allgemeine Lösung, bei der die letzten n Elemente verworfen werden (unter Verwendung einer Warteschlange, wie in den Kommentaren vorgeschlagen):

public static IEnumerable<T> SkipLastN<T>(this IEnumerable<T> source, int n) {
    var  it = source.GetEnumerator();
    bool hasRemainingItems = false;
    var  cache = new Queue<T>(n + 1);

    do {
        if (hasRemainingItems = it.MoveNext()) {
            cache.Enqueue(it.Current);
            if (cache.Count > n)
                yield return cache.Dequeue();
        }
    } while (hasRemainingItems);
}

static void Main(string[] args) {
    var Seq = Enumerable.Range(1, 4);

    Console.WriteLine(string.Join(", ", Seq.Select(x => x.ToString()).ToArray()));
    Console.WriteLine(string.Join(", ", Seq.SkipLastN(3).Select(x => x.ToString()).ToArray()));
}

7
Können Sie es jetzt verallgemeinern, um alle außer dem letzten n zu nehmen?
Eric Lippert

4
Nett. Ein kleiner Fehler; Die Warteschlangengröße sollte auf n + 1 initialisiert werden, da dies die maximale Größe der Warteschlange ist.
Eric Lippert

ReSharper versteht Ihren Code nicht ( Zuweisung in bedingten Ausdrücken ), aber ich mag es irgendwie +1
Грозный

43

Als Alternative zum Erstellen einer eigenen Methode und in einem Fall, in dem die Reihenfolge der Elemente nicht wichtig ist, funktioniert die nächste:

var result = sequence.Reverse().Skip(1);

49
Beachten Sie, dass dies erfordert, dass Sie über genügend Speicher verfügen, um die gesamte Sequenz zu speichern. Natürlich wird die gesamte Sequenz NOCH zweimal wiederholt, einmal, um die umgekehrte Sequenz zu erstellen, und einmal, um sie zu iterieren. Das ist so ziemlich schlimmer als die Count-Lösung, egal wie Sie sie in Scheiben schneiden. Es ist langsamer und benötigt viel mehr Speicher.
Eric Lippert

Ich weiß nicht, wie die 'Reverse'-Methode funktioniert, daher bin ich mir nicht sicher, wie viel Speicher verwendet wird. Aber ich stimme zwei Iterationen zu. Diese Methode sollte nicht für große Sammlungen verwendet werden oder wenn eine Leistung wichtig ist.
Kamarey

5
Wie würden Sie Reverse implementieren? Können Sie einen allgemeinen Weg finden , dies zu tun, ohne die gesamte Sequenz zu speichern?
Eric Lippert

2
Ich mag es und es erfüllt die Kriterien, die Sequenz nicht zweimal zu generieren.
Amy B

12
und außerdem müssen Sie die gesamte Sequenz erneut umkehren, um sie equence.Reverse().Skip(1).Reverse()
beizubehalten

42

Da ich kein Fan von expliziter Verwendung von bin Enumerator, ist hier eine Alternative. Beachten Sie, dass die Wrapper-Methoden erforderlich sind, damit ungültige Argumente frühzeitig ausgelöst werden, anstatt die Überprüfungen zu verschieben, bis die Sequenz tatsächlich aufgelistet ist.

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source)
{
    if (source == null)
        throw new ArgumentNullException("source");

    return InternalDropLast(source);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source)
{
    T buffer = default(T);
    bool buffered = false;

    foreach (T x in source)
    {
        if (buffered)
            yield return buffer;

        buffer = x;
        buffered = true;
    }
}

Gemäß dem Vorschlag von Eric Lippert lässt es sich leicht auf n Elemente verallgemeinern:

public static IEnumerable<T> DropLast<T>(this IEnumerable<T> source, int n)
{
    if (source == null)
        throw new ArgumentNullException("source");

    if (n < 0)
        throw new ArgumentOutOfRangeException("n", 
            "Argument n should be non-negative.");

    return InternalDropLast(source, n);
}

private static IEnumerable<T> InternalDropLast<T>(IEnumerable<T> source, int n)
{
    Queue<T> buffer = new Queue<T>(n + 1);

    foreach (T x in source)
    {
        buffer.Enqueue(x);

        if (buffer.Count == n + 1)
            yield return buffer.Dequeue();
    }
}

Wo ich jetzt vor dem Nachgeben statt nach dem Nachgeben puffere, so dass der n == 0Fall keine besondere Behandlung benötigt.


Im ersten Beispiel wäre es wahrscheinlich etwas schneller, buffered=falsevor dem Zuweisen eine else-Klausel festzulegen buffer. Die Bedingung wird ohnehin bereits überprüft, dies würde jedoch vermeiden, dass bufferedjedes Mal durch die Schleife redundant eingestellt wird.
James

Kann mir jemand die Vor- und Nachteile gegenüber der ausgewählten Antwort sagen?
Sinjai

Was ist der Vorteil einer Implementierung in einer separaten Methode, bei der die Eingabeprüfungen fehlen? Außerdem würde ich einfach die einzelne Implementierung löschen und der mehrfachen Implementierung einen Standardwert geben.
jpmc26

@ jpmc26 Wenn Sie eine separate Methode einchecken, erhalten Sie die Validierung tatsächlich, sobald Sie aufrufen DropLast. Andernfalls erfolgt die Validierung nur, wenn Sie die Sequenz tatsächlich aufzählen (beim ersten Aufruf von MoveNextauf das Ergebnis IEnumerator). Es macht keinen Spaß, Fehler zu beheben, wenn zwischen dem Generieren IEnumerableund dem tatsächlichen Auflisten eine beliebige Menge an Code vorhanden sein könnte . Heutzutage würde ich InternalDropLastals innere Funktion von schreiben DropLast, aber diese Funktionalität gab es in C # nicht, als ich diesen Code vor 9 Jahren schrieb.
Joren

28

Die Enumerable.SkipLast(IEnumerable<TSource>, Int32)Methode wurde in .NET Standard 2.1 hinzugefügt. Es macht genau das, was Sie wollen.

IEnumerable<int> sequence = GetSequenceFromExpensiveSource();

var allExceptLast = sequence.SkipLast(1);

Von https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable.skiplast

Gibt eine neue Aufzählungssammlung zurück, die die Elemente aus der Quelle enthält, wobei die letzten Zählelemente der Quellensammlung weggelassen wurden.


2
Dies existiert auch in MoreLinq
Leperkawn

+1 für SkipLast. Ich wusste das nicht, da ich kürzlich von .NET Framework kam und sie sich nicht die Mühe machten, es dort hinzuzufügen.
PRMan

12

Nichts in der BCL (oder MoreLinq, glaube ich), aber Sie könnten Ihre eigene Erweiterungsmethode erstellen.

public static IEnumerable<T> TakeAllButLast<T>(this IEnumerable<T> source)
{
    using (var enumerator = source.GetEnumerator())
        bool first = true;
        T prev;
        while(enumerator.MoveNext())
        {
            if (!first)
                yield return prev;
            first = false;
            prev = enumerator.Current;
        }
    }
}

Dieser Code wird nicht funktionieren ... sollte wahrscheinlich sein if (!first)und first = falseaus dem if herausziehen .
Caleb

Dieser Code sieht aus. Die Logik scheint zu sein, "das Uninitialisierte previn der ersten Iteration zurückzugeben und danach für immer zu schleifen".
Phil Miller

Der Code scheint den Fehler "Kompilierungszeit" zu haben. Vielleicht möchten Sie es korrigieren. Aber ja, wir können einen Extender schreiben, der einmal iteriert und alle bis auf das letzte Element zurückgibt.
Manish Basantani

@Caleb: Du hast absolut Recht - ich habe das in großer Eile geschrieben! Jetzt behoben. @Amby: Ähm, ich bin mir nicht sicher, welchen Compiler du verwendest. Ich hatte nichts dergleichen. : P
Noldorin

@ RobertSchmidt Ups, ja. Ich habe usingjetzt eine Erklärung hinzugefügt .
Noldorin

7

Es wäre hilfreich, wenn .NET Framework mit einer solchen Erweiterungsmethode ausgeliefert würde.

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int count)
{
    var enumerator = source.GetEnumerator();
    var queue = new Queue<T>(count + 1);

    while (true)
    {
        if (!enumerator.MoveNext())
            break;
        queue.Enqueue(enumerator.Current);
        if (queue.Count > count)
            yield return queue.Dequeue();
    }
}

3

Eine leichte Erweiterung von Jorens eleganter Lösung:

public static IEnumerable<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
{
    int i = 0;
    var buffer = new Queue<T>(right + 1);

    foreach (T x in source)
    {
        if (i >= left) // Read past left many elements at the start
        {
            buffer.Enqueue(x);
            if (buffer.Count > right) // Build a buffer to drop right many elements at the end
                yield return buffer.Dequeue();    
        } 
        else i++;
    }
}
public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(0, n);
}
public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
{
    return source.Shrink(n, 0);
}

Wobei Shrink eine einfache Vorwärtszählung implementiert, um die ersten leftvielen Elemente zu löschen, und denselben verworfenen Puffer, um die letzten rightvielen Elemente zu löschen .


3

Wenn Sie keine Zeit haben, Ihre eigene Erweiterung einzuführen, gehen Sie wie folgt vor:

var next = sequence.First();
sequence.Skip(1)
    .Select(s => 
    { 
        var selected = next;
        next = s;
        return selected;
    });

2

Eine kleine Abweichung von der akzeptierten Antwort, die (für meinen Geschmack) etwas einfacher ist:

    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        // for efficiency, handle degenerate n == 0 case separately 
        if (n == 0)
        {
            foreach (var item in enumerable)
                yield return item;
            yield break;
        }

        var queue = new Queue<T>(n);
        foreach (var item in enumerable)
        {
            if (queue.Count == n)
                yield return queue.Dequeue();

            queue.Enqueue(item);
        }
    }

2

Wenn Sie das Countoder Lengtheines Aufzählers erhalten können, was Sie in den meisten Fällen können, dann einfachTake(n - 1)

Beispiel mit Arrays

int[] arr = new int[] { 1, 2, 3, 4, 5 };
int[] sub = arr.Take(arr.Length - 1).ToArray();

Beispiel mit IEnumerable<T>

IEnumerable<int> enu = Enumerable.Range(1, 100);
IEnumerable<int> sub = enu.Take(enu.Count() - 1);

Die Frage bezieht sich auf IEnumerables und Ihre Lösung ist, was OP zu vermeiden versucht. Ihr Code hat Auswirkungen auf die Leistung.
Nawfal

1

Warum nicht einfach .ToList<type>()in der Sequenz, dann zählen und nehmen, wie Sie es ursprünglich getan haben? Aber da es in eine Liste aufgenommen wurde, sollte es nicht zweimal eine teure Aufzählung durchführen. Richtig?


1

Die Lösung, die ich für dieses Problem verwende, ist etwas ausgefeilter.

Meine statische Klasse util enthält eine Erweiterungsmethode, MarkEnddie die T-items in EndMarkedItem<T>-items konvertiert. Jedes Element ist mit einem Extra gekennzeichnet int, das entweder 0 ist . oder (falls man sich besonders für die letzten 3 Elemente interessiert) -3 , -2 oder -1 für die letzten 3 Elemente.

Dies kann für sich genommen nützlich sein, z. B. wenn Sie eine Liste in einer einfachen foreachSchleife mit Kommas nach jedem Element mit Ausnahme der letzten 2 erstellen möchten , wobei auf das vorletzte Element ein Konjunktionswort folgt (z. B. " und "). oder " oder ") und das letzte Element, gefolgt von einem Punkt.

Um die gesamte Liste ohne die letzten n Elemente zu generieren , ButLastiteriert die Erweiterungsmethode einfach über das EndMarkedItem<T>s while EndMark == 0.

Wenn Sie nichts angeben tailLength, wird nur das letzte Element markiert (in MarkEnd()) oder gelöscht (in ButLast()).

Wie bei den anderen Lösungen funktioniert dies durch Puffern.

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

namespace Adhemar.Util.Linq {

    public struct EndMarkedItem<T> {
        public T Item { get; private set; }
        public int EndMark { get; private set; }

        public EndMarkedItem(T item, int endMark) : this() {
            Item = item;
            EndMark = endMark;
        }
    }

    public static class TailEnumerables {

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts) {
            return ts.ButLast(1);
        }

        public static IEnumerable<T> ButLast<T>(this IEnumerable<T> ts, int tailLength) {
            return ts.MarkEnd(tailLength).TakeWhile(te => te.EndMark == 0).Select(te => te.Item);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts) {
            return ts.MarkEnd(1);
        }

        public static IEnumerable<EndMarkedItem<T>> MarkEnd<T>(this IEnumerable<T> ts, int tailLength) {
            if (tailLength < 0) {
                throw new ArgumentOutOfRangeException("tailLength");
            }
            else if (tailLength == 0) {
                foreach (var t in ts) {
                    yield return new EndMarkedItem<T>(t, 0);
                }
            }
            else {
                var buffer = new T[tailLength];
                var index = -buffer.Length;
                foreach (var t in ts) {
                    if (index < 0) {
                        buffer[buffer.Length + index] = t;
                        index++;
                    }
                    else {
                        yield return new EndMarkedItem<T>(buffer[index], 0);
                        buffer[index] = t;
                        index++;
                        if (index == buffer.Length) {
                            index = 0;
                        }
                    }
                }
                if (index >= 0) {
                    for (var i = index; i < buffer.Length; i++) {
                        yield return new EndMarkedItem<T>(buffer[i], i - buffer.Length - index);
                    }
                    for (var j = 0; j < index; j++) {
                        yield return new EndMarkedItem<T>(buffer[j], j - index);
                    }
                }
                else {
                    for (var k = 0; k < buffer.Length + index; k++) {
                        yield return new EndMarkedItem<T>(buffer[k], k - buffer.Length - index);
                    }
                }
            }    
        }
    }
}

1
    public static IEnumerable<T> NoLast<T> (this IEnumerable<T> items) {
        if (items != null) {
            var e = items.GetEnumerator();
            if (e.MoveNext ()) {
                T head = e.Current;
                while (e.MoveNext ()) {
                    yield return head; ;
                    head = e.Current;
                }
            }
        }
    }

1

Ich denke nicht, dass es prägnanter werden kann - auch um sicherzustellen, dass IEnumerator<T>:

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source)
{
    using (var it = source.GetEnumerator())
    {
        if (it.MoveNext())
        {
            var item = it.Current;
            while (it.MoveNext())
            {
                yield return item;
                item = it.Current;
            }
        }
    }
}

Bearbeiten: technisch identisch mit dieser Antwort .



0

Sie könnten schreiben:

var list = xyz.Select(x=>x.Id).ToList();
list.RemoveAt(list.Count - 1);

0

Dies ist eine allgemeine und meiner Meinung nach elegante Lösung, die alle Fälle korrekt behandelt:

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

public class Program
{
    public static void Main()
    {
        IEnumerable<int> r = Enumerable.Range(1, 20);
        foreach (int i in r.AllButLast(3))
            Console.WriteLine(i);

        Console.ReadKey();
    }
}

public static class LinqExt
{
    public static IEnumerable<T> AllButLast<T>(this IEnumerable<T> enumerable, int n = 1)
    {
        using (IEnumerator<T> enumerator = enumerable.GetEnumerator())
        {
            Queue<T> queue = new Queue<T>(n);

            for (int i = 0; i < n && enumerator.MoveNext(); i++)
                queue.Enqueue(enumerator.Current);

            while (enumerator.MoveNext())
            {
                queue.Enqueue(enumerator.Current);
                yield return queue.Dequeue();
            }
        }
    }
}

-1

Mein traditioneller IEnumerableAnsatz:

/// <summary>
/// Skips first element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping first element</returns>
private IEnumerable<U> SkipFirstEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        for (;e.MoveNext();) yield return e.Current;
        yield return e.Current;
    }
}

/// <summary>
/// Skips last element of an IEnumerable
/// </summary>
/// <typeparam name="U">Enumerable type</typeparam>
/// <param name="models">The enumerable</param>
/// <returns>IEnumerable of type skipping last element</returns>
private IEnumerable<U> SkipLastEnumerable<U>(IEnumerable<U> models)
{
    using (var e = models.GetEnumerator())
    {
        if (!e.MoveNext()) return;
        yield return e.Current;
        for (;e.MoveNext();) yield return e.Current;
    }
}

Ihr SkipLastEnumerable ist zwar traditionell, aber fehlerhaft. Das erste zurückgegebene Element ist immer ein undefiniertes U - auch wenn "Modelle" 1 Element haben. Im letzteren Fall würde ich ein leeres Ergebnis erwarten.
Robert Schmidt

Außerdem ist IEnumerator <T> nicht verfügbar.
Robert Schmidt

Richtig / notiert. Danke dir.
Chibueze Opata

-1

Eine einfache Möglichkeit wäre, einfach in eine Warteschlange zu konvertieren und die Warteschlange zu verlassen, bis nur noch die Anzahl der Elemente übrig ist, die Sie überspringen möchten.

public static IEnumerable<T> SkipLast<T>(this IEnumerable<T> source, int n)
{
    var queue = new Queue<T>(source);

    while (queue.Count() > n)
    {
        yield return queue.Dequeue();
    }
}

Es wird Take verwendet, um die bekannte Anzahl von Gegenständen aufzunehmen. Und die Warteschlange für ausreichend große Aufzählungen ist schrecklich.
Sinatr

-2

Könnte sein:

var allBuLast = sequence.TakeWhile(e => e != sequence.Last());

Ich denke, es sollte wie "Wo" sein, aber die Reihenfolge beibehalten (?).


3
Das ist ein sehr ineffizienter Weg. Um die Sequenz auszuwerten. Last () muss die gesamte Sequenz durchlaufen, und zwar für jedes Element in der Sequenz. O (n ^ 2) Effizienz.
Mike

Du hast recht. Ich weiß nicht, was ich dachte, als ich diese XD schrieb. Sind Sie sicher, dass Last () die gesamte Sequenz durchläuft? Bei einigen Implementierungen von IEnumerable (z. B. Array) sollte dies O (1) sein. Ich habe die List-Implementierung nicht überprüft, aber es könnte auch einen "umgekehrten" Iterator geben, der im letzten Element beginnt und auch O (1) benötigt. Außerdem sollten Sie berücksichtigen, dass zumindest technisch gesehen O (n) = O (2n) ist. Wenn dieses Verfahren für die Leistung Ihrer Apps nicht unbedingt kritisch ist, würde ich mich an die viel klarere Reihenfolge halten. Take (sequence.Count () - 1) .Regards!
Guillermo Ares

@Mike Ich stimme dir nicht zu, Kumpel, sequence.Last () ist O (1), damit es nicht die gesamte Sequenz durchlaufen muss. stackoverflow.com/a/1377895/812598
GoRoS

1
@GoRoS, es ist nur O (1), wenn die Sequenz ICollection / IList implementiert oder ein Array ist. Alle anderen Sequenzen sind O (N). In meiner Frage gehe ich nicht davon aus, dass eine der O (1) Quellen
Mike

3
Die Sequenz kann mehrere Elemente enthalten, die diese Bedingung erfüllen. E == sequence.Last (), zum Beispiel new [] {1, 1, 1, 1}
Sergey Shandar

-2

Wenn Geschwindigkeit eine Voraussetzung ist, sollte dieser Weg der alten Schule der schnellste sein, auch wenn der Code nicht so flüssig aussieht, wie es linq machen könnte.

int[] newSequence = int[sequence.Length - 1];
for (int x = 0; x < sequence.Length - 1; x++)
{
    newSequence[x] = sequence[x];
}

Dies erfordert, dass die Sequenz ein Array ist, da sie eine feste Länge und indizierte Elemente hat.


2
Sie haben es mit einer IEnumerable zu tun, die keinen Zugriff auf Elemente über einen Index ermöglicht. Ihre Lösung funktioniert nicht. Vorausgesetzt, Sie machen es richtig, müssen Sie die Sequenz durchlaufen, um die Länge zu bestimmen, ein Array der Länge n-1 zuweisen und alle Elemente kopieren. - 1. 2n-1 Operationen und (2n-1) * (4 oder 8 Bytes) Speicher. Das geht nicht mal schnell.
Tarik

-4

Ich würde wahrscheinlich so etwas machen:

sequence.Where(x => x != sequence.LastOrDefault())

Dies ist eine Iteration mit der Überprüfung, dass es nicht jedes Mal die letzte ist.


3
Zwei Gründe, warum das nicht gut ist. 1) .LastOrDefault () erfordert die Iteration der gesamten Sequenz, und dies wird für jedes Element in der Sequenz (in .Where ()) aufgerufen. 2) Wenn die Sequenz [1,2,1,2,1,2] ist und Sie Ihre Technik angewendet haben, bleibt [1,1,1] übrig.
Mike
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.