Skip to main content

Advanced Language Features

C# continues to evolve with powerful features that enhance productivity, code quality, and performance. This section covers advanced language features that may not fit neatly into other categories.

Tuples and Deconstruction​

Tuples provide a lightweight way to group multiple values without defining a specific type.

Tuple Literals​

// Creating tuples
var person = ("John", 30);
(string Name, int Age) employee = ("Alice", 25);
(string Name, int Age, decimal Salary) manager = ("Bob", 40, 75000m);

// Accessing tuple elements
Console.WriteLine(person.Item1); // John
Console.WriteLine(employee.Name); // Alice
Console.WriteLine(manager.Salary); // 75000

Named Tuple Elements​

// Explicit naming
var product = (Name: "Laptop", Price: 999.99m);
Console.WriteLine($"{product.Name}: ${product.Price}");

// Implicit naming from variables
string name = "Tablet";
decimal price = 499.99m;
var item = (name, price);
Console.WriteLine($"{item.name}: ${item.price}");

Tuple Equality (C# 7.3+)​

var tuple1 = (Name: "John", Age: 30);
var tuple2 = (Name: "John", Age: 30);
var tuple3 = (Name: "Alice", Age: 25);

Console.WriteLine(tuple1 == tuple2); // True
Console.WriteLine(tuple1 == tuple3); // False

Tuple Deconstruction​

// Basic deconstruction
var point = (10, 20);
var (x, y) = point;
Console.WriteLine($"X: {x}, Y: {y}");

// Deconstruction with existing variables
int width, height;
(width, height) = (1920, 1080);

// Partial deconstruction with discard
var person = ("John", "Doe", 30);
var (firstName, _, age) = person;

Custom Deconstruction​

You can add deconstruction support to your own types:

public class Point
{
public int X { get; }
public int Y { get; }

public Point(int x, int y) => (X, Y) = (x, y);

// Deconstructor method
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

// Usage
var point = new Point(10, 20);
var (x, y) = point;

Indices and Ranges (C# 8+)​

Indices and ranges provide a concise syntax for accessing single elements or ranges of elements in a sequence.

Index Operator​

The ^ operator indicates an index from the end:

string[] colors = { "red", "green", "blue", "yellow", "purple" };

// Access from start (0-based)
string first = colors[0]; // "red"

// Access from end (^1 means "1 from the end")
string last = colors[^1]; // "purple"
string secondLast = colors[^2]; // "yellow"

// Create Index objects
Index start = 0;
Index end = ^1;
string firstColor = colors[start]; // "red"
string lastColor = colors[end]; // "purple"

Range Operator​

The .. operator creates a range:

string[] colors = { "red", "green", "blue", "yellow", "purple" };

// Get a range (start is inclusive, end is exclusive)
string[] middle = colors[1..4]; // ["green", "blue", "yellow"]

// Ranges with open ends
string[] fromStart = colors[..3]; // ["red", "green", "blue"]
string[] toEnd = colors[2..]; // ["blue", "yellow", "purple"]

// Range from Index objects
Index start = 1;
Index end = ^1;
string[] subset = colors[start..end]; // ["green", "blue", "yellow"]

// Create a Range object
Range middleRange = 1..4;
string[] middleColors = colors[middleRange]; // ["green", "blue", "yellow"]

// Copy the entire array
string[] allColors = colors[..]; // ["red", "green", "blue", "yellow", "purple"]

Custom Index and Range Support​

You can add support for indices and ranges to your own types:

public class MyCollection<T>
{
private readonly T[] _items;

public MyCollection(T[] items) => _items = items;

// Index support
public T this[Index index] => _items[index];

// Range support
public T[] this[Range range]
{
get
{
var (start, length) = range.GetOffsetAndLength(_items.Length);
var result = new T[length];
Array.Copy(_items, start, result, 0, length);
return result;
}
}
}

Nullable Reference Types (C# 8+)​

Nullable reference types help prevent null reference exceptions by making nullability explicit in the type system.

Enabling Nullable Reference Types​

// In a project file:
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

// Or at the file level:
#nullable enable

Nullable and Non-Nullable References​

// Non-nullable reference type (can't be null)
string nonNullable = "Hello";
nonNullable = null; // Warning: Assignment of null to non-nullable reference

// Nullable reference type (can be null)
string? nullable = "Hello";
nullable = null; // OK

// Warning for possible null dereference
string? name = GetName();
int length = name.Length; // Warning: Possible null reference exception

// Null check to suppress warning
if (name != null)
{
int length = name.Length; // OK
}

// Null-conditional operator
int? length = name?.Length;

// Null-forgiving operator (use with caution)
int length = name!.Length; // "Trust me, it's not null"

Nullable Contexts​

You can control nullable analysis with directives:

#nullable enable // Enable nullable reference types
#nullable disable // Disable nullable reference types
#nullable restore // Restore to project setting

#nullable disable warnings // Keep annotations but disable warnings
#nullable enable warnings // Re-enable warnings

// Example
#nullable disable
public class LegacyCode
{
// No nullable warnings here
}

#nullable enable
public class ModernCode
{
// Nullable warnings active here
}

Required Members (C# 11+)​

Required members ensure that properties or fields are initialized during object creation.

public class Person
{
// These properties must be initialized when creating a Person
public required string FirstName { get; set; }
public required string LastName { get; set; }

// Optional property
public int? Age { get; set; }
}

// Usage
var person = new Person
{
FirstName = "John", // Required
LastName = "Doe" // Required
// Age is optional
};

SetsRequiredMembers Attribute​

public class Person
{
public required string FirstName { get; set; }
public required string LastName { get; set; }

// This constructor sets all required members
[SetsRequiredMembers]
public Person(string fullName)
{
var parts = fullName.Split(' ');
FirstName = parts[0];
LastName = parts[1];
}
}

// Can now create without explicitly setting required properties
var person = new Person("John Doe");

Init-only Properties (C# 9+)​

Init-only properties can only be set during object initialization, making objects immutable after creation.

public class ImmutablePerson
{
// Can only be set during initialization
public string FirstName { get; init; }
public string LastName { get; init; }
public DateTime DateOfBirth { get; init; }

// Calculated property
public int Age => DateTime.Now.Year - DateOfBirth.Year;
}

// Usage
var person = new ImmutablePerson
{
FirstName = "John",
LastName = "Doe",
DateOfBirth = new DateTime(1990, 1, 1)
};

// Error: Init-only property can only be set in initializer
// person.FirstName = "Jane";

Record Types (C# 9+)​

Records are reference types designed for immutable data models with value-based equality.

Record Classes​

// Basic record
public record Person(string FirstName, string LastName);

// Usage
var person1 = new Person("John", "Doe");
var person2 = new Person("John", "Doe");

// Value-based equality
Console.WriteLine(person1 == person2); // True
Console.WriteLine(person1.Equals(person2)); // True

// Built-in ToString
Console.WriteLine(person1); // Person { FirstName = John, LastName = Doe }

// Deconstruction
var (firstName, lastName) = person1;

// With-expression for non-destructive mutation
var person3 = person1 with { LastName = "Smith" };

Record with Additional Members​

public record Employee(string Name, decimal Salary)
{
// Additional properties
public string Department { get; init; } = "General";
public int YearsOfService { get; init; }

// Methods
public decimal CalculateBonus() => Salary * 0.1m * YearsOfService;

// Custom deconstructor
public void Deconstruct(out string name, out decimal salary, out string department)
{
name = Name;
salary = Salary;
department = Department;
}
}

Record Structs (C# 10+)​

// Value type record
public record struct Point(double X, double Y)
{
// Additional members
public double DistanceFromOrigin => Math.Sqrt(X * X + Y * Y);
}

// Usage
var p1 = new Point(3, 4);
var p2 = new Point(3, 4);

Console.WriteLine(p1 == p2); // True
Console.WriteLine(p1.DistanceFromOrigin); // 5

Pattern Matching​

Pattern matching allows for more expressive and concise code when testing and extracting values.

Type Patterns​

object obj = "Hello, World!";

// Type pattern
if (obj is string message)
{
Console.WriteLine(message.ToUpper());
}

// Switch expression with type patterns
string GetDescription(object obj) => obj switch
{
string s => $"String of length {s.Length}",
int i => $"Integer with value {i}",
DateTime d => $"Date: {d.ToShortDateString()}",
null => "Null value",
_ => "Unknown type"
};

Property Patterns​

// Property pattern
if (person is { Age: >= 18, IsActive: true })
{
Console.WriteLine("Active adult user");
}

// Nested property patterns
if (order is { Customer: { IsVip: true }, TotalAmount: > 1000 })
{
Console.WriteLine("VIP order with high value");
}

// Switch expression with property patterns
string GetShippingMethod(Order order) => order switch
{
{ TotalAmount: > 1000, Customer.IsVip: true } => "Express",
{ TotalAmount: > 500 } => "Priority",
{ Customer.IsVip: true } => "Standard+",
_ => "Standard"
};

Tuple Patterns​

// Tuple pattern
(int x, int y) = GetCoordinates();
string quadrant = (x, y) switch
{
(> 0, > 0) => "First quadrant",
(< 0, > 0) => "Second quadrant",
(< 0, < 0) => "Third quadrant",
(> 0, < 0) => "Fourth quadrant",
(0, 0) => "Origin",
(_, 0) => "X-axis",
(0, _) => "Y-axis",
_ => "Unknown"
};

Logical Patterns​

// Logical patterns (and, or, not)
if (person is { Age: > 18 and < 65 })
{
Console.WriteLine("Working age");
}

string GetCategory(Product product) => product switch
{
{ Price: > 1000 } or { IsLuxury: true } => "Premium",
{ InStock: false } => "Unavailable",
{ Category: "Books" or "Electronics" } and { Price: < 50 } => "Budget",
_ => "Standard"
};

List Patterns (C# 11+)​

// List patterns
int[] numbers = { 1, 2, 3, 4, 5 };

bool IsFirstThree = numbers is [1, 2, 3, ..]; // True
bool StartsWithOne = numbers is [1, ..]; // True
bool HasFiveElements = numbers is [_, _, _, _, _]; // True
bool EndsWithFive = numbers is [.., 5]; // True

// Switch expression with list patterns
string DescribeArray(int[] arr) => arr switch
{
[] => "Empty array",
[var single] => $"Single element: {single}",
[var first, var second] => $"Two elements: {first}, {second}",
[1, 2, 3] => "Array with exactly 1, 2, 3",
[1, .., 5] => "Array starting with 1 and ending with 5",
[> 0, > 0, .. ] => "Array starting with two positive numbers",
_ => "Other array"
};

Discard Patterns​

// Discard pattern (_)
string GetSign(int number) => number switch
{
> 0 => "Positive",
< 0 => "Negative",
_ => "Zero"
};

// Var pattern with discard
if (dictionary.TryGetValue("key", out var _))
{
Console.WriteLine("Key exists");
}

Resources​