Unity3d OneLine – problemy, które da się rozwiązać w jednej linii kodu. Dosłownie!

Linia: Jak sortować tablice i listy obiektów w Unity3d (C#)

Tablice w programowaniu są bardzo przydatne. To nie podlega żadnej wątpliwości. Jednak często się zdarza, że potrzebujemy taką tablicę posortować. O ile w przypadku prostej tablicy (z danymi typu standardowego jak int czy string) nie ma problemu. O tyle, kiedy tablice wypełniają bardziej złożone obiekty wszystko się nieco komplikuje.

Jak zwykle tworzymy pojedynczy skrypt C# i przypisujemy go do kamery.

W przypadku prostych tablic kod sortowania wygląda tak:

using UnityEngine;
using System;
using System.Collections;

public class OneLine_05_01 : MonoBehaviour {

	private int[] values = new int[5];

	void Start() {
		values [0] = 5;
		values [1] = 1;
		values [2] = 3;
		values [3] = 7;
		values [4] = 2;

		foreach (float value in values) {
			Debug.Log(value);
		}

		Debug.Log ("SORTOWANIE");
		Array.Sort (values);

		foreach (float value in values) {
			Debug.Log(value);
		}
	}
}

Warto tutaj zwrócić uwagę na konieczność dodania using System; Bez tego, kod nie znajdzie obiektu Array, a my dostaniemy błędem.

Gdy zamiast tablicy, skorzystamy z listy, kod będzie wyglądał tak:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class OneLine_05_01 : MonoBehaviour {

	private List<int> values = new List<int> ();

	void Start() {
		values.Add (3);
		values.Add (1);
		values.Add (7);
		values.Add (5);
		values.Add (4);

		foreach (float value in values) {
			Debug.Log(value);
		}

		Debug.Log ("SORTOWANIE");
		values.Sort ();

		foreach (float value in values) {
			Debug.Log(value);
		}
	}
}

Tutaj znów, ważnej jest dodanie using System.Collections.Generic;

Czym różni się tablica od listy? Tablica po utworzeniu ma stały rozmiar, nie da się jej łatwo zmieniać, widać to przy dodawaniu elementów i deklaracji tablicy/listy. Przy tablicy podajemy konkretny rozmiar i wartości przypisujemy pod konkretny indeks. W liście, dodajemy póki nam się podoba.

Przy używaniu prostych typów (jak int, string, float) zaleca się korzystać z tablicy. Odczyt z tablicy potrafi być 6-krotnie szybszy, niż odczyt z listy. Więc nasza aplikacja sporo zyskuje. Jednak, gdy zamierzamy przechowywać obiekty złożone, różnica ta, nie jest odczuwalna, więc można spokojnie korzystać z wygodniejszych list.

Zważywszy na powyższe, nasze sortowanie obiektów, przećwiczymy sobie na liście. Tworzymy taki kod:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class OneLine_05_01 : MonoBehaviour {

	private class complexObject {

		public string name;
		public int weight;
		public int price;

		public complexObject(string n, int w, int p) {
			this.name = n;
			this.weight = w;
			this.price = p;
		}
	}

	private List<complexObject> values = new List<complexObject> ();

	void Start() {
		values.Add (new complexObject("Miecz", 2, 100));
		values.Add (new complexObject("Topor", 6, 120));
		values.Add (new complexObject("Sztylet", 1, 10));
		values.Add (new complexObject("Zbroja", 20, 600));
		values.Add (new complexObject("Luk", 4, 160));


		foreach (complexObject value in values) {
			Debug.Log(value.name + " - " + value.weight + " - " + value.price);
		}

		Debug.Log ("SORTOWANIE");
		values.Sort ();

		foreach (complexObject value in values) {
			Debug.Log(value.name + " - " + value.weight + " - " + value.price);
		}
	}
}

Na samym początku tworzymy klasę na której będziemy ćwiczyć. Jest to prosta klasa z 3 polami i konstruktorem. Reszta kodu praktycznie się nie zmieniła. Jedynie w pętlach, float został zmieniony przez nazwę klasy (complexObject). No i zmieniło się dodawanie obiektów do listy. Dzięki konstruktorowi, dodajemy je sobie bardzo prosto, bo przy tworzeniu instancji, od razu ustalamy wartości pól.

Jednak, gdy wykonamy ten kod, dostaniemy taki oto błąd:

Błąd przy wykonaniu
Błąd przy wykonaniu

Co on oznacza? Że kompilator nie ma pojęcia jak porównać ze sobą obiekty typu complexObject. No bo w sumie jak? Ma tam 3 różne pola, wg. czego ma porównywać? Modyfikujemy więc nasz kod:

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

public class OneLine_05_01 : MonoBehaviour {

	private class complexObject {

		public string name;
		public int weight;
		public int price;

		public complexObject(string n, int w, int p) {
			this.name = n;
			this.weight = w;
			this.price = p;
		}
	}

	private List<complexObject> values = new List<complexObject> ();

	void Start() {
		values.Add (new complexObject("Miecz", 2, 100));
		values.Add (new complexObject("Topor", 6, 120));
		values.Add (new complexObject("Sztylet", 1, 10));
		values.Add (new complexObject("Zbroja", 20, 600));
		values.Add (new complexObject("Luk", 4, 160));


		foreach (complexObject value in values) {
			Debug.Log(value.name + " - " + value.weight + " - " + value.price);
		}

		Debug.Log ("SORTOWANIE");
		values = values.OrderBy( go => go.weight ).ToList();

		foreach (complexObject value in values) {
			Debug.Log(value.name + " - " + value.weight + " - " + value.price);
		}
	}
}

Co się zmieniło? Dodaliśmy dodatkową bibliotekę using System.Linq;, żeby móc skorzystać z funkcji OrderBy. Wewnątrz funkcji, wskazujemy na pole, wg. którego będziemy sortować. U mnie padło na wagę.

Jak rozumieć tą śmieszną konstrukcję go => go.weight ? Nie wchodząc w szczegóły, określamy, jakie pole obiektu, będzie go reprezentować w procesie porównywania.

Jeżeli jednak chcemy korzystać z tablicy, możemy wykorzystać ten sam patent, ale na końcu trzeba zastąpić “ToList()”, na: “ToArray()”.

Nie są to wszystkie metody sortowania. Może dokonać ręcznego (samodzielnego) sortowania, pisząc swój algorytm. Czy to Bąbelkowy, czy QuickSort. Z reguły polega to na podaniu do funkcji tablicy, gdzie sami odpowiadamy za sprytne porównywanie i ustawianie obiektów w tablicy wynikowej. (Polecam sobie poczytać o tym).

OrderBy vs Sort

Jest jeszcze jedna ciekawa kwestia, jaką jest funkcja Sort. Po co nam ona? Otóż, funkcja OrderBy ma jedną znaczącą wadę. Kopiuje całą naszą tablicę. Czyli w pamięci trzymamy 2 wersję naszej tablicy – posortowaną i nieposortowaną. Przez to, że od razu do naszej zmiennej przypisujemy wartość tablicy posortowanej, to ta nieposortowana zniknie z pamięci (gdy GarbageCollector ją usunie).

Ta “wada” może być przydatna, gdy oryginalny format tablicy będzie nam jeszcze potrzebny. Jednak, gdy go nie potrzebujemy, możemy wykorzystać prostszą konstrukcję:

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

public class OneLine_05_01 : MonoBehaviour {

	private class complexObject {

		public string name;
		public int weight;
		public int price;

		public complexObject(string n, int w, int p) {
			this.name = n;
			this.weight = w;
			this.price = p;
		}
	}

	private List<complexObject> values = new List<complexObject> ();

	void Start() {
		values.Add (new complexObject("Miecz", 2, 100));
		values.Add (new complexObject("Topor", 6, 120));
		values.Add (new complexObject("Sztylet", 1, 10));
		values.Add (new complexObject("Zbroja", 20, 600));
		values.Add (new complexObject("Luk", 4, 160));


		foreach (complexObject value in values) {
			Debug.Log(value.name + " - " + value.weight + " - " + value.price);
		}

		Debug.Log ("SORTOWANIE");
		values.Sort((a, b) => a.price.CompareTo(b.price));

		foreach (complexObject value in values) {
			Debug.Log(value.name + " - " + value.weight + " - " + value.price);
		}
	}
}

Jak widać, przypisanie stało się zbędne, bo operacja sortowania, dokonuje się “na żywo” na naszej tablicy. Jednak, znów jako parametr funkcji, musimy podać zasady porównywania. Wygląda to bardzo podobnie do OrderBy, jednak po lewej stronie przypisania (=>) mamy parę, która reprezentuje dwa obiekty. Po prawej znów zostaje zasada porównania. Kiedy powiem, że CompareTo znaczy “porównaj do”, cała konstrukcja powinna być jasna. Price to oczywiście nazwa pola w naszej klasie.

W przypadkach, kiedy chcemy porównywać “mieszankę zmiennych”. Np. u nas sortować po ilorazie ceny z wagą, można napisać własną funkcję porównującą. Ale tym zajmiemy się innym razem.

Done!