Gdy w kodzie potrzebuję utworzyć kopię tablicy w C#, najpierw sprawdzam, czy chodzi o nową tablicę, czy tylko o drugi dostęp do tych samych danych. To rozróżnienie decyduje o wyborze między przypisaniem referencji, Array.Copy, Clone, LINQ i nowszą składnią kolekcji. Poniżej pokazuję praktyczne przykłady, różnice między kopiowaniem płytkim i głębokim oraz typowe pułapki.
Najszybszy wybór metody zależy od rodzaju danych
- Array.Copy sprawdza się przy klasycznym kopiowaniu całej tablicy lub jej fragmentu.
- Clone() tworzy kopię płytką i jest wygodny przy tablicach wartości.
- ToArray() dobrze pasuje do potoków LINQ i zwraca nową tablicę.
- [.. tablica] to czytelna składnia dostępna od C# 12.
- Przy elementach będących obiektami sama kopia tablicy nie wystarczy do uzyskania kopii głębokiej.

Najpierw rozdziel referencję od kopii danych
Najczęstszy błąd wygląda niewinnie i polega na zwykłym przypisaniu tablicy do drugiej zmiennej:
int[] oryginalna = { 1, 2, 3 };
int[] kopia = oryginalna;
kopia[0] = 99;
Console.WriteLine(oryginalna[0]); // 99Obie zmienne wskazują wtedy na ten sam obiekt w pamięci. Nie powstała żadna kopia, więc zmiana wykonana przez `kopia` jest widoczna także przez `oryginalna`. W praktyce taki kod ma sens tylko wtedy, gdy świadomie chcemy udostępnić tę samą tablicę.
Prawdziwa kopia tworzy nową instancję tablicy i przepisuje do niej elementy. Dla typów wartości, takich jak `int`, `double` czy `DateTime`, efekt jest zwykle intuicyjny. Przy typach referencyjnych kopiowane są jednak często same referencje do obiektów, a nie obiekty znajdujące się pod tymi referencjami.
Najpraktyczniejsze sposoby kopiowania tablicy
Array.Copy przy pełnej tablicy i wybranym zakresie
Najbardziej uniwersalnym rozwiązaniem jest Array.Copy. Najpierw tworzymy tablicę docelową, a potem przepisujemy do niej elementy:
int[] oryginalna = { 10, 20, 30 };
int[] kopia = new int[oryginalna.Length];
Array.Copy(oryginalna, kopia, oryginalna.Length);
kopia[0] = 99;
Console.WriteLine(oryginalna[0]); // 10
Console.WriteLine(kopia[0]); // 99Ta metoda jest szczególnie przydatna, gdy kopiuję tylko fragment danych. Parametry określają tablicę źródłową, indeks początkowy, tablicę docelową, indeks docelowy i liczbę elementów:
int[] oryginalna = { 10, 20, 30, 40, 50 };
int[] fragment = new int[3];
Array.Copy(oryginalna, 1, fragment, 0, 3);
// fragment zawiera: 20, 30, 40Przy kopiowaniu fragmentów trzeba pilnować długości tablicy docelowej oraz zakresu indeksów. Jeśli zabraknie miejsca albo któryś indeks wyjdzie poza tablicę, otrzymamy ArgumentException lub ArgumentOutOfRangeException.
Clone jako krótka kopia płytka
Metoda `Clone()` jest bardzo zwięzła i tworzy nową tablicę o takim samym rozmiarze. Ponieważ zwraca typ `object`, w kodzie często pojawia się rzutowanie:
int[] oryginalna = { 1, 2, 3 };
int[] kopia = (int[])oryginalna.Clone();
kopia[1] = 200;
Console.WriteLine(oryginalna[1]); // 2W przypadku tablicy liczb otrzymujemy niezależne wartości, ale technicznie jest to kopiowanie płytkie. Dla tablicy obiektów nowa jest sama tablica, natomiast jej elementy nadal mogą wskazywać na te same instancje.
ToArray w kodzie korzystającym z LINQ
Jeżeli tablica jest częścią operacji LINQ, `ToArray()` zwykle czyta się najlepiej:
using System.Linq;
int[] oryginalna = { 1, 2, 3, 4 };
int[] kopia = oryginalna.ToArray();Metoda tworzy nową tablicę i kopiuje do niej elementy. Nie używam jej jednak bez powodu w prostym, gorącym fragmencie kodu, ponieważ wymaga LINQ i może zaciemnić prostą operację. Jej mocną stroną jest naturalne zakończenie potoku przekształceń:
int[] liczby = { 1, 2, 3, 4, 5 };
int[] kopia = liczby
.Where(x => x % 2 == 1)
.Select(x => x * 10)
.ToArray();
// kopia zawiera: 10, 30, 50Składnia spread dostępna od C# 12
W nowszym C# tablicę można skopiować za pomocą składni kolekcji:
int[] oryginalna = { 1, 2, 3 };
int[] kopia = [.. oryginalna];To rozwiązanie jest krótkie i dobrze pokazuje intencję. Używam go chętnie w nowym kodzie, o ile projekt korzysta z C# 12 lub nowszego. W starszym projekcie lepiej wybrać `Array.Copy`, `Clone()` albo `ToArray()`, zależnie od kontekstu.
Przeczytaj również: Enum w C# - jak używać go bezpiecznie?
CopyTo i Span przy świadomym kopiowaniu pamięci
Tablica ma również metodę `CopyTo`, ale wymaga wcześniej utworzonej tablicy docelowej:
int[] oryginalna = { 1, 2, 3 };
int[] kopia = new int[oryginalna.Length];
oryginalna.CopyTo(kopia, 0);W kodzie nastawionym na pracę z buforami można użyć `Span
int[] oryginalna = { 1, 2, 3 };
int[] kopia = new int[oryginalna.Length];
oryginalna.AsSpan().CopyTo(kopia);Nie traktowałbym jednak `Span
Którą metodę wybrać w konkretnym przypadku
Różnice między metodami są niewielkie przy małych tablicach, ale ich znaczenie rośnie, gdy kod jest często wykonywany albo kopiuje duże bufory. Najlepiej dobrać rozwiązanie do intencji, a nie do samej długości zapisu.
| Metoda | Najlepsze zastosowanie | Najważniejsze ograniczenie |
|---|---|---|
Array.Copy |
Pełna tablica lub wybrany fragment | Trzeba utworzyć tablicę docelową |
Clone() |
Krótka kopia tablicy tego samego typu | Zwraca object i jest płytka |
ToArray() |
Końcowy etap operacji LINQ | Wymaga LINQ i tworzy nową tablicę |
[.. źródło] |
Nowoczesny, czytelny kod w C# 12+ | Nie pasuje do starszych wersji języka |
Span |
Bufory, zakresy i kod wydajnościowy | Może być przesadą przy prostych operacjach |
Moja praktyczna reguła jest prosta. Do zwykłej kopii tablicy wartości wybieram Array.Copy albo składnię spread. Gdy pracuję w LINQ, korzystam z `ToArray()`, a gdy kopiuję konkretny zakres, sięgam po przeciążenie `Array.Copy` z indeksami.
Kopia płytka i głęboka przy tablicach obiektów
Sprawa komplikuje się, gdy tablica przechowuje obiekty klas:
class Produkt
{
public string Nazwa { get; set; } = "";
}
Produkt[] oryginalna =
{
new Produkt { Nazwa = "Klawiatura" }
};
Produkt[] kopia = (Produkt[])oryginalna.Clone();
kopia[0].Nazwa = "Mysz";
Console.WriteLine(oryginalna[0].Nazwa); // MyszTablice są różne, ale oba elementy wskazują na ten sam obiekt Produkt. `Clone()`, `Array.Copy`, `ToArray()` i składnia spread kopiują tutaj referencję. Nie tworzą automatycznie nowych produktów.
Kopia głęboka wymaga jawnego skopiowania każdego elementu oraz jego zagnieżdżonych danych. Najczęściej robię to przez konstruktor kopiujący, metodę fabrykującą albo mapowanie:
Produkt[] kopia = oryginalna
.Select(produkt => new Produkt
{
Nazwa = produkt.Nazwa
})
.ToArray();W tym przykładzie `string` nie wymaga dodatkowego kopiowania, ponieważ jest typem niemutowalnym. Gdy `Produkt` ma na przykład listę kategorii albo obiekt adresu, trzeba utworzyć również nowe instancje tych zagnieżdżonych elementów. Samo `Select` bez tworzenia nowych obiektów nie rozwiąże problemu.
Tablice wielowymiarowe i zagnieżdżone
Tablica dwuwymiarowa z typami wartości może zostać skopiowana tak samo jak tablica jednowymiarowa:
int[,] macierz =
{
{ 1, 2 },
{ 3, 4 }
};
int[,] kopia = (int[,])macierz.Clone();
kopia[0, 0] = 99;
Console.WriteLine(macierz[0, 0]); // 1Więcej uwagi wymaga tablica postrzępiona, czyli tablica tablic. Przy takim kodzie:
int[][] oryginalna =
{
new[] { 1, 2 },
new[] { 3, 4 }
};
int[][] kopia = (int[][])oryginalna.Clone();skopiowana zostaje tylko tablica zewnętrzna. Wewnętrzne wiersze nadal są współdzielone. Aby uzyskać niezależną kopię, trzeba skopiować każdy wiersz osobno:
int[][] kopia = oryginalna
.Select(wiersz => wiersz.ToArray())
.ToArray();
kopia[0][0] = 99;
Console.WriteLine(oryginalna[0][0]); // 1To jeden z przypadków, w których krótki `Clone()` może dać pozornie poprawny, ale błędny rezultat. Przy strukturach zagnieżdżonych zawsze sprawdzam, na którym poziomie kończy się kopiowanie.
Typowe błędy, których łatwo uniknąć
Poza przypisaniem referencji często spotykam nieprawidłowe użycie `Array.Copy`. Tablica docelowa musi mieć wystarczający rozmiar, a liczba kopiowanych elementów nie może przekraczać dostępnego zakresu:
int[] źródło = { 1, 2, 3 };
int[] cel = new int[2];
Array.Copy(źródło, cel, źródło.Length); // błądJeśli potrzebujemy tylko dwóch pierwszych elementów, liczba kopiowanych wartości musi wynosić `2`. Jeśli potrzebujemy całej tablicy, rozmiar celu powinien pochodzić z `źródło.Length`, a nie z ręcznie wpisanej liczby.
Drugie nieporozumienie dotyczy wydajności. Nie zakładam z góry, że `Buffer.BlockCopy` będzie najlepszy. Jest przeznaczony głównie do kopiowania danych z tablic typów prostych na poziomie bajtów i łatwo użyć go niewłaściwie. W typowym kodzie biznesowym Array.Copy jest bezpieczniejszym i czytelniejszym wyborem.
Trzeba też pamiętać o `null`. Wywołanie `oryginalna.Clone()` albo `oryginalna.ToArray()` dla pustej referencji zakończy się wyjątkiem. Jeżeli brak tablicy jest poprawnym stanem, obsłuż go jawnie zamiast maskować problem przypadkowym pustym wynikiem.
Najlepsza kopia to czasem brak kopiowania
Jeżeli funkcja ma tylko odczytać dane, tworzenie nowej tablicy może być niepotrzebnym kosztem. W takim przypadku rozważam przekazanie `ReadOnlySpan
Kopię tworzę wtedy, gdy potrzebuję niezależnego cyklu życia danych, ochrony przed zmianami albo osobnego bufora roboczego. Dla typowej tablicy wartości wystarczy Array.Copy, Clone lub składnia [..]; przy obiektach i strukturach zagnieżdżonych trzeba już świadomie zaprojektować kopiowanie głębokie. To właśnie ta decyzja, a nie sama długość kodu, najczęściej decyduje o poprawności rozwiązania.
