This episode expands your knowledge of C# types: struct, enum, tuple, and nullable value types, collections like Array, List, Dictionary, and HashSet, LINQ basics for querying collections, and nullable reference types for modern null safety.

In episode 4 you mastered OOP with classes and records. Now we broaden our type vocabulary. Production developers spend most of their time working with data collections — and C# provides a very complete toolset for this.
Understanding when to use a struct versus a class, when to use a List versus a HashSet, and how to express data that may have no value will determine the quality and safety of your code.
Episode 5 covers the advanced types: struct, enum, tuple, and nullable types, then the core collections, followed by LINQ basics, and closes with nullable reference types, which are enabled by default in modern projects.
Unlike a class, which is a reference type, a struct is a value type — its value is copied when it's moved. Structs suit small, immutable data such as coordinates or quantities:
struct Titik
{
public int X { get; }
public int Y { get; }
public Titik(int x, int y) => (X, Y) = (x, y);
}
enum Ukuran
{
Kecil = 1,
Sedang = 2,
Besar = 3
}
var p = new Titik(10, 20);
var ukuran = Ukuran.Besar;
Console.WriteLine($"{p.X},{p.Y} ukuran {(int)ukuran}");A tuple is a concise way to group several values without creating a new class. It's useful for simple return values:
(int Min, int Max) Batas() => (1, 100);
var batas = Batas();
Console.WriteLine($"Min {batas.Min}, Max {batas.Max}");The tuple (int Min, int Max) names its elements, making the code more readable. For data used across methods and classes, still choose a record.
Value types like int cannot normally hold a null value. For situations where the absence of a value is meaningful — such as a date of birth that hasn't been filled in — use a nullable value type with a question mark:
int? umur = null;
umur = 30;
if (umur.HasValue)
{
Console.WriteLine($"Umur {umur.Value}");
}
else
{
Console.WriteLine("Umur belum diisi.");
}Safe access can be shortened with umur ?? 0 — the null-coalescing operator returns a default value when the variable is null.
These four collections cover almost every need:
Array: fixed size, fast indexing.List<T>: a dynamic list that grows as needed.Dictionary<TKey, TValue>: fast lookup by key.HashSet<T>: a set of unique values with set operations.var angka = new[] { 1, 2, 3 };
var produk = new List<string> { "Kopi", "Teh" };
produk.Add("Susu");
var stok = new Dictionary<string, int>
{
["Kopi"] = 10,
["Teh"] = 5
};
var idUnik = new HashSet<int> { 1, 2, 2, 3 };
Console.WriteLine($"Stok kopi {stok["Kopi"]}, unik {idUnik.Count}");Notice that HashSet rejects duplicates: idUnik.Count is 3, not 4. Choose the data structure according to your access patterns — Dictionary and HashSet offer O(1) average lookup.
LINQ changes how you work with collections. Instead of writing manual loops, you describe what you want, not how:
var nilaiSiswa = new List<int> { 55, 78, 90, 62, 88 };
var lulus = nilaiSiswa
.Where(n => n >= 70)
.OrderByDescending(n => n)
.ToList();
Console.WriteLine($"Lulus {lulus.Count} siswa: {string.Join(", ", lulus)}");The operators Where, OrderByDescending, and ToList are examples of deferred execution — a query is only executed when its results are actually needed. nilaiSiswa.Where(n => n >= 70) produces an IEnumerable that is evaluated when iterated.
Since C# 8, nullable reference types (NRT) warn you when you might misuse null. New projects enable them through the Nullable property in the .csproj file. Here's how to read them: string means must not be null, string? means may be null:
string nama = "Arman"; // tidak boleh null
string? kota = null; // boleh null
if (kota is not null)
{
Console.WriteLine($"{nama} tinggal di {kota}");
}The compiler will warn you if you use kota without checking for null first. The is not null guard asserts that inside that block, the value is definitely present.
Key takeaways:
?? operator handle values that may be absent.In the next episode 6 we deal with the unwanted but inevitable: exception handling and debugging — try-catch-finally, throw and custom exceptions, using declarations for resources, debugging in Visual Studio and VS Code, and basic logging.