Skip to main content

C# 13 Features

Overview

C# 13 introduces several powerful features focused on performance, safety, and modern programming patterns.

params Collections

Traditional params (C# 1.0)

// Old way - only arrays
public void PrintItems(params string[] items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}

PrintItems("A", "B", "C"); // Allocates array on heap

New params Collections (C# 13)

// New way - works with any collection type!
public void PrintItems(params IEnumerable<string> items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}

// Works with Span - no heap allocation!
public void ProcessNumbers(params ReadOnlySpan<int> numbers)
{
int sum = 0;
foreach (var num in numbers)
{
sum += num;
}
Console.WriteLine($"Sum: {sum}");
}

ProcessNumbers(1, 2, 3, 4, 5); // Stack-allocated!

// Works with List
public void AddItems(params List<string> items)
{
_storage.AddRange(items);
}

// Works with collection expressions
public void Configure(params ReadOnlySpan<string> options) { }
Configure(["Option1", "Option2", "Option3"]);

Performance Benefits

// ❌ Old way - always heap allocates
public int Sum(params int[] numbers)
{
return numbers.Sum();
}

Sum(1, 2, 3); // Allocates int[] on heap

// ✅ New way - can be stack-allocated
public int Sum(params ReadOnlySpan<int> numbers)
{
int sum = 0;
foreach (var n in numbers)
sum += n;
return sum;
}

Sum(1, 2, 3); // No heap allocation!

Real-World Example

public class Logger
{
// High-performance logging without allocations
public void LogFormat(string template, params ReadOnlySpan<object> args)
{
// Process without allocating array
Span\<char\> buffer = stackalloc char[1024];
int pos = 0;

foreach (var arg in args)
{
// Build log message in stack buffer
}
}
}

// Usage
logger.LogFormat("User {0} logged in at {1}", userId, timestamp);

New Lock Type and Semantics

The Problem with Old lock

// ❌ Dangerous - can lock on anything
public class BadLocking
{
private string _lockObj = "lock"; // BAD: strings are interned

public void DoWork()
{
lock (_lockObj) // Can cause unintended locking across instances!
{
// Critical section
}
}
}

// ❌ Boxing issues
public class MoreBadLocking
{
private int _lockObj = 42; // BAD: value type

public void DoWork()
{
lock (_lockObj) // Boxes the int - different box each time!
{
// Not actually thread-safe!
}
}
}

New System.Threading.Lock Type (C# 13)

using System.Threading;

public class SafeLocking
{
private readonly Lock _lock = new Lock(); // New Lock type

public void DoWork()
{
lock (_lock) // Type-safe, designed for locking
{
// Critical section
}
}

public void TryWork()
{
// Supports scoped patterns
using (_lock.EnterScope())
{
// Critical section
}
}
}

Benefits of New Lock Type

public class ModernLocking
{
private readonly Lock _lock = new();

// ✅ Better performance - optimized for locking scenarios
// ✅ Better debugging - lock contention tracking
// ✅ Better static analysis - compiler can warn about misuse
// ✅ Scoped locking pattern support

public void ProcessData()
{
using var scope = _lock.EnterScope();

// Critical section
// Lock automatically released when scope is disposed
}
}

Lock Object Requirements (C# 13 Enforcement)

// ✅ GOOD - appropriate lock objects
private readonly object _lock1 = new object();
private readonly Lock _lock2 = new Lock();
private readonly SemaphoreSlim _lock3 = new SemaphoreSlim(1);

// ❌ BAD - compiler warnings in C# 13
private string _lock4 = "bad"; // Warning: don't lock on strings
private Type _lock5 = typeof(MyClass); // Warning: don't lock on Type
lock (this) { } // Warning: don't lock on this

Relaxed ref struct Constraints

Old Limitations (C# before 13)

// ❌ ref struct couldn't implement interfaces
public ref struct OldSpanWrapper // Error in C# 12
{
private Span<byte> _data;
}

public ref struct OldSpanWrapper : IDisposable // ❌ Not allowed
{
// ...
}

New Capabilities (C# 13)

// ✅ ref struct can now implement interfaces!
public ref struct SpanWrapper : IDisposable
{
private Span<byte> _data;

public void Dispose()
{
_data.Clear();
}
}

// ✅ Can be used in some generic contexts
public ref struct ValueList\<T\> : IEnumerable\<T\>
{
private Span\<T\> _items;

public IEnumerator\<T\> GetEnumerator()
{
// Implementation
}
}

Real-World Example: High-Performance Parsers

public interface IParser\<T\>
{
bool TryParse(ReadOnlySpan\<char\> input, out T result);
}

// Now possible in C# 13!
public ref struct IntParser : IParser<int>
{
private ReadOnlySpan\<char\> _buffer;

public IntParser(ReadOnlySpan\<char\> buffer)
{
_buffer = buffer;
}

public bool TryParse(ReadOnlySpan\<char\> input, out int result)
{
return int.TryParse(input, out result);
}
}

Partial Properties and Indexers

Partial Properties

// Declaration file
public partial class User
{
public partial string Name { get; set; }
}

// Implementation file
public partial class User
{
private string _name;

public partial string Name
{
get => _name;
set => _name = value?.Trim() ?? string.Empty;
}
}

Partial Indexers

public partial class DataStore
{
public partial string this[int index] { get; set; }
}

public partial class DataStore
{
private Dictionary<int, string> _data = new();

public partial string this[int index]
{
get => _data.TryGetValue(index, out var value) ? value : string.Empty;
set => _data[index] = value;
}
}

New Escape Sequence \e

Usage

// Before C# 13
string ansiRed = "\u001b[31m"; // Unicode escape

// C# 13 - cleaner
string ansiRed = "\e[31m"; // Escape character

// Examples
Console.WriteLine($"\e[31mRed text\e[0m"); // Red text
Console.WriteLine($"\e[1;32mBold green\e[0m"); // Bold green
Console.WriteLine($"\e[4;34mUnderlined blue\e[0m"); // Underlined blue

Overload Resolution Priority

Problem

// Library version 1
public class Math
{
public static int Max(int a, int b) => a > b ? a : b;
}

// User code
var result = Math.Max(5, 10);

// Library version 2 adds generic overload
public class Math
{
public static int Max(int a, int b) => a > b ? a : b;
public static T Max\<T\>(T a, T b) where T : IComparable\<T\> // Breaks user code!
=> a.CompareTo(b) > 0 ? a : b;
}

Solution: OverloadResolutionPriority (C# 13)

using System.Runtime.CompilerServices;

public class Math
{
// Lower priority - only used when int overload doesn't match
[OverloadResolutionPriority(0)]
public static T Max\<T\>(T a, T b) where T : IComparable\<T\>
=> a.CompareTo(b) > 0 ? a : b;

// Higher priority - preferred for int arguments
[OverloadResolutionPriority(1)]
public static int Max(int a, int b) => a > b ? a : b;
}

// User code still works - int overload is preferred
var result = Math.Max(5, 10); // Calls int overload

Interview Questions

Q: What's the benefit of params collections over params arrays?

A: params collections can work with any collection type, including Span which enables stack allocation instead of heap allocation. This eliminates allocations in hot paths and improves performance.

Q: Why is the new Lock type better than using object?

A: The Lock type is purpose-built for synchronization with better performance, better debugging support, clearer intent, and prevents common mistakes like locking on strings or this.

Q: What problem do relaxed ref struct constraints solve?

A: They allow ref structs to implement interfaces and be used in more generic contexts, enabling high-performance abstractions that were previously impossible. This is useful for parsers, formatters, and other zero-allocation scenarios.

Q: When would you use OverloadResolutionPriority?

A: When building libraries that need to add new overloads without breaking existing code. It lets you control which overload is preferred when multiple match.

Practice Exercises

  1. Convert a method using params array to params Span and measure performance
  2. Implement a thread-safe cache using the new Lock type
  3. Create a ref struct that implements IDisposable for resource cleanup
  4. Build a high-performance parser using params ReadOnlySpan<char>
  5. Demonstrate the new \e escape sequence with colored console output