ToList () - czy tworzy nową listę?

176

Powiedzmy, że mam zajęcia

public class MyObject
{
   public int SimpleInt{get;set;}
}

I mam List<MyObject>, a ja ToList()to, a następnie zmieniam jedną z SimpleInt, czy moja zmiana zostanie propagowana z powrotem do oryginalnej listy. Innymi słowy, jaki byłby wynik następującej metody?

public void RunChangeList()
{
  var objs = new List<MyObject>(){new MyObject(){SimpleInt=0}};
  var whatInt = ChangeToList(objs );
}
public int ChangeToList(List<MyObject> objects)
{
  var objectList = objects.ToList();
  objectList[0].SimpleInt=5;
  return objects[0].SimpleInt;

}

Czemu?

P / S: Przepraszam, jeśli wydaje się to oczywiste. Ale teraz nie mam ze sobą kompilatora ...

Graviton
źródło
Jednym ze sposobów wyrażenia tego jest to, że .ToList()jest to płytka kopia. Odniesienia są kopiowane, ale nowe odniesienia nadal wskazują te same wystąpienia, na które wskazują oryginalne odniesienia. Kiedy się nad tym zastanowić, ToListnie można stworzyć żadnego, new MyObject()kiedy MyObjectjest classtypem.
Jeppe Stig Nielsen

Odpowiedzi:

217

Tak, ToListutworzy nową listę, ale ponieważ w tym przypadku MyObjectjest to typ referencyjny, nowa lista będzie zawierała odniesienia do tych samych obiektów, co oryginalna lista.

Aktualizacja SimpleIntwłaściwości obiektu, do którego odwołuje się nowa lista, wpłynie również na równoważny obiekt na pierwotnej liście.

(Gdyby MyObjectzostała zadeklarowana jako a, structa nie a, classwówczas nowa lista zawierałaby kopie elementów z pierwotnej listy, a zaktualizowanie właściwości elementu na nowej liście nie wpłynęłoby na równoważny element na pierwotnej liście).

LukeH
źródło
1
Należy również pamiętać, że w przypadku Liststruktur z przypisanie takie jak objectList[0].SimpleInt=5nie byłoby dozwolone (błąd czasu kompilacji C #). Dzieje się tak, ponieważ wartość zwracana metody dostępu indeksatora listy getnie jest zmienną (jest to zwrócona kopia wartości struktury), a zatem ustawienie jej elementu członkowskiego .SimpleIntza pomocą wyrażenia przypisania jest niedozwolone (spowodowałoby to mutację kopii, która nie jest przechowywana) . Cóż, kto w ogóle używa mutowalnych struktur?
Jeppe Stig Nielsen,
2
@Jeppe Stig Nielson: Tak. I podobnie, kompilator również przestanie Ci robić takie rzeczy jak foreach (var s in listOfStructs) { s.SimpleInt = 42; }. Naprawdę paskudny haczyka jest, kiedy próbują coś takiego listOfStructs.ForEach(s => s.SimpleInt = 42): kompilator pozwala go i uruchamia kod bez wyjątków, ale elemencie na liście pozostaną niezmienione!
LukeH
62

Ze źródła Reflectora:

public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
{
    if (source == null)
    {
        throw Error.ArgumentNull("source");
    }
    return new List<TSource>(source);
}

Więc tak, Twoja oryginalna lista nie zostanie zaktualizowana (tj. Dodana lub usunięta), ale obiekty, do których istnieją odniesienia, będą.

Chris S.
źródło
32

ToList zawsze utworzy nową listę, która nie będzie odzwierciedlać żadnych późniejszych zmian w kolekcji.

Jednak będzie odzwierciedlać zmiany w samych obiektach (chyba że są to struktury zmienne).

Innymi słowy, jeśli zastąpisz obiekt z oryginalnej listy innym obiektem, ToListnadal będzie on zawierał pierwszy obiekt.
Jeśli jednak zmodyfikujesz jeden z obiektów na oryginalnej liście, ToListnadal będzie zawierał ten sam (zmodyfikowany) obiekt.

SLaks
źródło
12

Przyjęta odpowiedź poprawnie odpowiada na pytanie PO na jego przykładzie. Jednak ma ToListto zastosowanie tylko wtedy, gdy jest stosowane do konkretnej kolekcji; nie zachowuje się, gdy elementy sekwencji źródłowej nie zostały jeszcze utworzone (z powodu odroczonego wykonania). W tym drugim przypadku możesz otrzymać nowy zestaw elementów za każdym razem, gdy dzwonisz ToList(lub wyliczasz sekwencję).

Oto adaptacja kodu PO w celu zademonstrowania tego zachowania:

public static void RunChangeList()
{
    var objs = Enumerable.Range(0, 10).Select(_ => new MyObject() { SimpleInt = 0 });
    var whatInt = ChangeToList(objs);   // whatInt gets 0
}

public static int ChangeToList(IEnumerable<MyObject> objects)
{
    var objectList = objects.ToList();
    objectList.First().SimpleInt = 5;
    return objects.First().SimpleInt;
}

Chociaż powyższy kod może wydawać się wymyślony, to zachowanie może pojawić się jako subtelny błąd w innych scenariuszach. Zobacz mój inny przykład sytuacji, w której powoduje to wielokrotne pojawianie się zadań.

Douglas
źródło
11

Tak, tworzy nową listę. Jest to zgodne z projektem.

Lista będzie zawierać te same wyniki, co oryginalna wyliczalna sekwencja, ale zmaterializowana w trwałej kolekcji (w pamięci). Pozwala to na wielokrotne wykorzystanie wyników bez ponoszenia kosztów ponownego obliczenia sekwencji.

Piękno sekwencji LINQ polega na tym, że można je komponować. Często IEnumerable<T>wynik jest wynikiem połączenia wielu operacji filtrowania, porządkowania i / lub projekcji. Metody rozszerzające, takie jak ToList()i ToArray()umożliwiają konwersję obliczonej sekwencji do standardowej kolekcji.

LBushkin
źródło
6

Tworzona jest nowa lista, ale pozycje w niej zawarte są odniesieniami do pozycji oryginalnych (tak jak na pierwotnej liście). Zmiany w samej liście są niezależne, ale w elementach zmiany zostaną znalezione na obu listach.

Preet Sangha
źródło
6

Po prostu natknąłem się na ten stary post i pomyślałem o dodaniu moich dwóch centów. Generalnie, jeśli mam wątpliwości, szybko używam metody GetHashCode () na dowolnym obiekcie, aby sprawdzić tożsamość. Więc powyżej -

    public class MyObject
{
    public int SimpleInt { get; set; }
}


class Program
{

    public static void RunChangeList()
    {
        var objs = new List<MyObject>() { new MyObject() { SimpleInt = 0 } };
        Console.WriteLine("objs: {0}", objs.GetHashCode());
        Console.WriteLine("objs[0]: {0}", objs[0].GetHashCode());
        var whatInt = ChangeToList(objs);
        Console.WriteLine("whatInt: {0}", whatInt.GetHashCode());
    }

    public static int ChangeToList(List<MyObject> objects)
    {
        Console.WriteLine("objects: {0}", objects.GetHashCode());
        Console.WriteLine("objects[0]: {0}", objects[0].GetHashCode());
        var objectList = objects.ToList();
        Console.WriteLine("objectList: {0}", objectList.GetHashCode());
        Console.WriteLine("objectList[0]: {0}", objectList[0].GetHashCode());
        objectList[0].SimpleInt = 5;
        return objects[0].SimpleInt;

    }

    private static void Main(string[] args)
    {
        RunChangeList();
        Console.ReadLine();
    }

I odpowiedz na moim komputerze -

  • objs: 45653674
  • objs [0]: 41149443
  • obiekty: 45653674
  • obiekty [0]: 41149443
  • objectList: 39785641
  • objectList [0]: 41149443
  • whatInt: 5

Więc zasadniczo obiekt, który zawiera lista, pozostaje taki sam w powyższym kodzie. Mam nadzieję, że takie podejście pomoże.

vibhu
źródło
4

Myślę, że jest to równoważne zapytaniu, czy ToList robi głęboką lub płytką kopię. Ponieważ ToList nie ma możliwości sklonowania MyObject, musi wykonać płytką kopię, więc utworzona lista zawiera te same odwołania co oryginalna, więc kod zwraca 5.

Timory
źródło
2

ToList utworzy zupełnie nową listę.

Jeśli pozycje na liście są typami wartości, zostaną bezpośrednio zaktualizowane, jeśli są typami odwołań, wszelkie zmiany zostaną odzwierciedlone z powrotem w obiektach, do których istnieją odniesienia.

Oded
źródło
2

W przypadku, gdy obiekt źródłowy jest prawdziwym IEnumerable (tj. Nie jest tylko kolekcją spakowaną jako wyliczalna), ToList () NIE może zwrócić tych samych odwołań do obiektów, jak w oryginalnym IEnumerable. Zwróci nową listę obiektów, ale te obiekty mogą nie być takie same lub nawet równe obiektom uzyskanym przez IEnumerable po ponownym wyliczeniu

prmph
źródło
1
 var objectList = objects.ToList();
  objectList[0].SimpleInt=5;

Spowoduje to również zaktualizowanie oryginalnego obiektu. Nowa lista będzie zawierała odniesienia do zawartych w niej obiektów, tak jak oryginalna lista. Możesz zmienić elementy albo, a aktualizacja zostanie odzwierciedlona w innych.

Teraz, jeśli zaktualizujesz listę (dodasz lub usuniesz element), który nie zostanie odzwierciedlony na drugiej liście.

kemiller2002
źródło
1

Nie widzę nigdzie w dokumentacji, że ToList () zawsze gwarantuje zwrócenie nowej listy. Jeśli IEnumerable jest listą, skuteczniejsze może być sprawdzenie tego i po prostu zwrócenie tej samej listy.

Martwisz się, że czasami możesz chcieć mieć absolutną pewność, że zwrócona lista jest! = Do oryginalnej listy. Ponieważ Microsoft nie dokumentuje, że ToList zwróci nową Listę, nie możemy być pewni (chyba że ktoś znalazł tę dokumentację). Może się to również zmienić w przyszłości, nawet jeśli działa teraz.

new List (IEnumerable enumerablestuff) gwarantuje, że zwróci nową Listę. Zamiast tego użyłbym tego.

Ben B.
źródło
2
Mówi o tym w dokumentacji tutaj: „ Metoda ToList <TSource> (IEnumerable <TSource>) wymusza natychmiastową ocenę zapytania i zwraca List <T> zawierający wyniki zapytania. Możesz dołączyć tę metodę do zapytania, aby uzyskać kopia wyników zapytania zapisana w pamięci podręcznej. ”Drugie zdanie stanowi powtórzenie, że jakiekolwiek zmiany pierwotnej listy po ocenie zapytania nie mają wpływu na zwróconą listę.
Raymond Chen
1
@RaymondChen Dotyczy to IEnumerables, ale nie list. Chociaż ToListwydaje się tworzyć nowe odniesienie do obiektu listy, gdy wywoływane jest na a List, BenB ma rację, mówiąc, że nie jest to gwarantowane przez dokumentację MS.
Teejay
@RaymondChen Zgodnie ze źródłem ChrisS, IEnumerable<>.ToList()jest faktycznie zaimplementowany jako new List<>(source)i nie ma określonego nadpisania dla List<>, więc w List<>.ToList()rzeczywistości zwraca nowe odniesienie do obiektu listy. Ale znowu, zgodnie z dokumentacją MS, nie ma gwarancji, że to się nie zmieni w przyszłości, chociaż prawdopodobnie spowoduje to uszkodzenie większości kodu.
Teejay