Mimo, iż nie jest to blog poświęcony programowaniu w języku C#, częściej lub rzadziej programista hurtowni danych jest zmuszony do napisania kilku linijek kodu w tym języku lub sprawdzenia jakiegoś istniejącego fragmentu kodu. Zwykle będzie to miało miejsce podczas pracy z SSIS i wykonaniu pewnych mniej standardowych transformacji lub logiki, ewentualnie w przypadku czytania z mniej oczywistych źródeł danych. Najpewniej będzie to zatem “Script Task” lub “Scrip Component” w SQL Server Integration Services, ale nie tylko. Napisanie tych kilku linijek może być dość prostym zadaniem, natomiast czasami może się okazać, że kod mimo iż działa to nie działa on poprawnie. Czasami taki błąd może zwracać nieprawidłowe wyniki, natomiast różnica pomiędzy błędnymi i prawidłowymi wynikami będzie tak niewielka, że ciężko będzie ją odnaleźć nawet “na produkcji”. I nie mówię tutaj o błędach w typach danych i zaokrągleniach, chociaż tutaj też można by napisać podobny post… W tym chciałbym pokazać jedno z niebezpieczeństw na które możemy natrafić podczas korzystania z List<Generic> Natrafiłem na taki problem już w dwóch różnych projektach, więc być może nie jest on tak niszowy i warto go opisać.
Problem
Załóżmy, że mamy kolekcję wartości, którą następnie musimy poddać pewnej filtracji i/lub zmienić jej zawartość. Scenariusz chyba nie jest bardzo abstrakcyjny i ja spotkałem się osobiście z podobnym kodem w przypadku pobierania danych z WebServic’u, którego jeden z parametrów był dynamiczny i musiał być dodatkowo filtrowany. Spotkałem się również z podobnym scenariuszem w przypadku autorskiego pakietu do synchronizacji ról dla kostki. Do filtracji kolekcji autor przygotował zatem stosowną metodę, która wyglądała w uproszczeniu tak jak ta poniżej.
static List<string> SomeMethod(List<String> someList) { List<string> currentList = someList; // do something... currentList.Remove("one"); return currentList; }
Przekazujemy zatem do parametru listę (kolekcję) – “someList”, natomiast na podstawie pewnej logiki zmieniamy wartości w “nowej” kolekcji – “currentList”. W tym przypadku usuwamy jeden, konkretny element. Jako wynik zwracana jest właśnie ta nowa kolekcja.
static void Main(string[] args) { List<String> myList = new List<string>() { "one", "two", "three", "four"}; Console.WriteLine("Number of elements in origin list: {0}", myList.Count()); List<String> outputList = SomeMethod(myList); Console.WriteLine("Number of elements in origin list: {0}, number of elements in output list: {1}", myList.Count(), outputList.Count()); Console.ReadKey(); }
Następnie możemy wywołać naszą metodę oraz uzyskać nową kolekcję. Spodziewanym rezultatem w tym przypadku powinna być kolekcja pomniejszona o jeden, wcześniej usunięty element. Zerknijmy jednak na wynik konsoli.
Pierwsze sprawdzenie zwróciło 4, czyli pierwotną liczbę elementów. Było to sprawdzenie zaraz po inicjalizacji tej kolekcji i wszystko wygląda w porządku. Następne rezultaty mogą już jednak być nieco zaskakujące. Widzimy, że nowa lista posiada obecnie 3 elementy co jest poprawne, natomiast stara kolekcja również została zmieniona. To zachowanie już nie jest tym czego spodziewał się autor tego kodu. Dlaczego tak się stało? Powodem jest fakt, iż List<T> jest typem referencyjnym. Bez wchodzenia w dalsze szczegóły dociekliwych odsyłam do tych dwóch miejsc:
- https://stackoverflow.com/questions/36558959/is-list-a-value-type-or-a-reference-type
- https://stackoverflow.com/questions/5057267/what-is-the-difference-between-a-reference-type-and-value-type-in-c
W skrócie można powiedzieć, że zmienna nie przechowuje w pamięci konkretnych wartości, natomiast referencje do tych wartości. Tak więc stworzenie tej nowej listy (kolekcji) po prostu utworzyło nową referencję i obie listy były od siebie zależne.
Rozwiązanie
Jeżeli chcielibyśmy natomiast uzyskać to co autor chciał uzyskać – i co pewnie dla większości z nas w takiej sytuacji będzie zamierzonym celem – powinniśmy zmodyfikować naszą funkcję. Rozwiązania są dwa. Pierwsze, które zadziała, natomiast nie jest do końca prawidłowe to skorzystanie z poniższej konstrukcji, czyli jawne zadeklarowanie nowej kolekcji za pomocą składni “new List<T>”
static List<string> AnotherMethod(List<string> someList) { List<string> currentList = new List<string>(someList); // do something... currentList.Remove("one"); return currentList; }
W tym przypadku wynik działania metody będzie zgodny z tym czego oczekiwaliśmy, czyli nowa kolekcja posiada obecnie 3 elementy, natomiast stara kolekcja została niezmieniona.
Dlaczego zatem rozwiązanie to nie jest idealne? Otóż okazuje się, że zadziała ono tylko w przypadku prostych typów (string, int, itd.), natomiast w przypadku złożonych obiektów (oparte na klasach), które same w sobie są typami referencyjnymi efekt będzie taki sam jak poprzednio. Dlatego też najbezpieczniejszym rozwiązaniem – dla każdego typu zmiennych – będzie niniejsza składnia i użycie “AddRange”.
static List<string> AndAnotherMethod(List<string> someList) { List<string> currentList = new List<string>; currentList.AddRange(someList); // do something... currentList.Remove("one"); return currentList; }
W tym przypadku tak naprawdę kopiujemy wartości z jednej kolekcji i dodajemy ją do drugiej kolekcji. Kod będzie działał zarówno dla prostych jak i złożonych typów.
Jak widać na powyższym przykładzie osoby, które nie mają dużego doświadczenia w programowaniu mogą dość łatwo dać się złapać w pewne pułapki. Oczywiście w zależności od danych błąd ten prędzej czy później zostanie wychwycony, natomiast przez długi czas może działać powodując drobne rozbieżności lub nieprawidłowości. Miejmy zatem oczy szeroko otwarte i porządnie testujmy zwłaszcza coś, w czym nie czujemy się zbyt pewni. Mam nadzieję, że ten post pomoże Wam ustrzec się jednego z niebezpieczeństw w C# dla programisty hurtowni danych.
- Docker dla amatora danych – Tworzenie środowiska (VM) w Azure i VirtualBox (skrypt) - April 20, 2020
- Power-up your BI project with PowerApps – materiały - February 5, 2020
- Docker dla “amatora” danych – kod źródłowy do prezentacji - November 18, 2019
Last comments