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
- Convert a method using params array to params Span and measure performance
- Implement a thread-safe cache using the new Lock type
- Create a ref struct that implements IDisposable for resource cleanup
- Build a high-performance parser using params ReadOnlySpan<char>
- Demonstrate the new \e escape sequence with colored console output
Related Concepts
- Lock vs Monitor
- Span<T> and Memory patterns