Skip to main content

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

  1. Create an inline array-based circular buffer for logging
  2. Design a type alias system for a Result/Either pattern
  3. Build a fluent query builder using default lambda parameters
  4. Implement a high-performance data processor using inline arrays and Span<T>