Inline Arrays, Type Aliases, and Lambda Defaults (C# 12)
Inline Arrays
Overview
Inline arrays are a feature for creating fixed-size array buffers within structs, designed for high-performance scenarios where you need stack-allocated arrays.
Syntax
[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer10\<T\>
{
private T _element0;
}
// Usage
Buffer10<int> buffer;
buffer[0] = 1;
buffer[1] = 2;
int value = buffer[0];
Key Concepts
1. Performance Benefits
// ❌ Heap allocation
public class DataProcessor
{
private int[] _buffer = new int[100]; // Heap allocated
}
// ✅ Stack allocation with inline array
[InlineArray(100)]
public struct InlineBuffer
{
private int _element;
}
public class DataProcessor
{
private InlineBuffer _buffer; // Stack allocated
}
2. Real-World Example: Ring Buffer
[InlineArray(1024)]
public struct RingBuffer\<T\>
{
private T _element;
}
public class Logger
{
private RingBuffer<string> _logBuffer;
private int _position;
public void Log(string message)
{
_logBuffer[_position % 1024] = message;
_position++;
}
public ReadOnlySpan<string> GetRecentLogs()
{
var buffer = _logBuffer;
return MemoryMarshal.CreateReadOnlySpan(ref buffer[0], 1024);
}
}
When to Use Inline Arrays
✅ Use for:
- Fixed-size buffers in high-performance code
- Avoiding heap allocations
- Low-level data structures
- Interop with native code
❌ Avoid for:
- Variable-size collections
- Public APIs (complexity)
- General application code
Type Aliases (Enhanced using directive)
Overview
C# 12 expands using aliases to work with any type, including tuples, nullable types, and complex generic types.
Syntax
// Alias tuple types
using Coordinate = (double Latitude, double Longitude);
using UserInfo = (int Id, string Name, string Email);
// Alias nullable types
using OptionalInt = int?;
using NullableGuid = System.Guid?;
// Alias complex generic types
using StringDict = System.Collections.Generic.Dictionary<string, string>;
using IntList = System.Collections.Generic.List<int>;
// Alias generic types with constraints
using Result\<T\> = System.ValueTuple<bool Success, T Value, string Error>;
Practical Examples
1. Simplifying Tuple Types
using Point3D = (double X, double Y, double Z);
using RGB = (byte Red, byte Green, byte Blue);
public class Graphics
{
public Point3D CreatePoint(double x, double y, double z) => (x, y, z);
public RGB MixColors(RGB color1, RGB color2)
{
return (
(byte)((color1.Red + color2.Red) / 2),
(byte)((color1.Green + color2.Green) / 2),
(byte)((color1.Blue + color2.Blue) / 2)
);
}
}
2. Simplifying Generic Collections
using UserCache = System.Collections.Concurrent.ConcurrentDictionary<int, User>;
using EventQueue = System.Collections.Concurrent.ConcurrentQueue<Event>;
public class EventSystem
{
private readonly UserCache _users = new();
private readonly EventQueue _events = new();
public void AddUser(int id, User user) => _users[id] = user;
public void QueueEvent(Event evt) => _events.Enqueue(evt);
}
3. Result Pattern
using Result = (bool Success, string Error);
using Result\<T\> = (bool Success, T Value, string Error);
public class ValidationService
{
public Result ValidateEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return (false, "Email is required");
if (!email.Contains('@'))
return (false, "Invalid email format");
return (true, string.Empty);
}
public Result<User> GetUser(int id)
{
var user = _repository.FindById(id);
if (user == null)
return (false, default, "User not found");
return (true, user, string.Empty);
}
}
Benefits
- Readability: Complex types have meaningful names
- Maintainability: Change the aliased type in one place
- Consistency: Use same pattern throughout codebase
- Reduced verbosity: Shorter, clearer code
Default Lambda Parameters
Overview
C# 12 allows lambda expressions to have default parameter values, similar to regular methods.
Syntax
// Basic default parameters
var greet = (string name = "Guest") => $"Hello, {name}!";
Console.WriteLine(greet()); // "Hello, Guest!"
Console.WriteLine(greet("Alice")); // "Hello, Alice!"
// Multiple parameters with defaults
var calculate = (int x, int y = 10, int z = 5) => x + y + z;
Console.WriteLine(calculate(1)); // 16 (1 + 10 + 5)
Console.WriteLine(calculate(1, 2)); // 8 (1 + 2 + 5)
Console.WriteLine(calculate(1, 2, 3)); // 6 (1 + 2 + 3)
Practical Examples
1. Configuration Callbacks
public class ServiceBuilder
{
public ServiceBuilder Configure(
Action<Options> configure = null)
{
var options = new Options();
configure?.Invoke(options);
return this;
}
}
// Usage with default
var builder = new ServiceBuilder();
builder.Configure(); // Uses default (null, no configuration)
builder.Configure(opt => opt.Timeout = 30); // Custom configuration
2. Event Handlers
public class Button
{
public event Action<string, bool> Clicked;
public void SetClickHandler(
Action<string, bool> handler = (message = "Clicked", enabled = true)
=> Console.WriteLine($"{message}, Enabled: {enabled}"))
{
Clicked = handler;
}
}
3. Fluent APIs
public class QueryBuilder
{
public QueryBuilder Where(
Func<string, string, string> condition =
(column = "Id", op = "=") => $"{column} {op} ?")
{
// Build query
return this;
}
}
4. Optional Transformation Pipelines
public class DataProcessor\<T\>
{
public IEnumerable\<T\> Process(
IEnumerable\<T\> data,
Func<T, bool> filter = null,
Func<T, T> transform = (x) => x) // Identity by default
{
var filtered = filter != null ? data.Where(filter) : data;
return filtered.Select(transform);
}
}
// Usage
var processor = new DataProcessor<int>();
var result = processor.Process([1, 2, 3, 4, 5]); // No filtering, no transformation
var doubled = processor.Process([1, 2, 3], transform: x => x * 2); // Just transformation
When to Use Default Lambda Parameters
✅ Use for:
- Optional configuration callbacks
- Providing sensible defaults for transformations
- Simplifying fluent APIs
- Event handlers with default behaviors
❌ Avoid for:
- Complex default logic (use regular methods)
- When defaults aren't obvious
- Public API design (clarity over cleverness)
Interview Questions
Q: When would you use inline arrays instead of regular arrays?
A: Inline arrays are used for high-performance scenarios where you need fixed-size buffers without heap allocation. Use them for buffers in hot paths, interop scenarios, or when you need stack allocation guarantees. Regular arrays are better for most application code.
Q: How do type aliases improve code quality?
A: Type aliases improve readability by giving complex types meaningful names, improve maintainability by centralizing type definitions, and reduce verbosity in code. They're especially useful for complex tuples and generic types.
Q: Can default lambda parameters have complex default values?
A: Yes, but keep them simple. Default parameters should be compile-time constants or simple expressions. Complex logic belongs in the lambda body or a separate method.
Practice Exercises
- Create an inline array-based circular buffer for logging
- Design a type alias system for a Result/Either pattern
- Build a fluent query builder using default lambda parameters
- Implement a high-performance data processor using inline arrays and Span<T>
Related Concepts
- Span<T> and Memory<T>
- Collection Expressions
- Delegates and Lambdas