On November 12, 2024, Microsoft launched .NET 9 and C# 13, bringing exciting updates for developers. The new features in C# 13 are all about making coding faster, smoother, and more efficient. Whether you’re an experienced coder or just starting out, these updates are designed to help you write better code with less hassle. Let’s take a closer look at what’s new and how it can make a difference in your projects.

1. Params

Params Keyword 

The params keyword in C# allows passing a variable number of arguments to a method without needing to create an array. This is helpful when the number of arguments is not fixed. 

Example Code:

				
					public void PrintNumbers(params int[] numbers) 
{ 
    foreach (var number in numbers) 
    { 
        Console.WriteLine(number); 
    } 
}
				
			

// Usage 
PrintNumbers(1, 2, 3, 4, 5); // Output: 1 2 3 4 5

Collections 

You can use collections (like List<T> or Dictionary<TKey, TValue>) to pass multiple parameters to methods. 

Example Code:

				
					public void PrintNames(List<string> names) 
{ 
    foreach (var name in names) 
    { 
        Console.WriteLine(name); 
    } 
}
				
			

// Usage 
PrintNames(new List<string> { “Alice”, “Bob”, “Charlie” });

Tuples 

Tuples allow grouping multiple values into a single object.

Example Code:

				
					public void DisplayInfo((string Name, int Age) person) 
{ 
    Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); 
}
				
			

// Usage 
DisplayInfo((“Alice”, 30));

Custom Classes

You can create custom classes to encapsulate multiple parameters.

Example Code:

				
					public class Person 
{ 
    public string Name { get; set; } 
    public int Age { get; set; } 
}
public void DisplayPerson(Person person) 
{ 
    Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); 
}
				
			

// Usage 
DisplayPerson(new Person { Name = “Alice”, Age = 30 });

Span<T> and ReadOnlySpan<T> in C# 

Span<T> and ReadOnlySpan<T> allow working with contiguous memory regions efficiently, without extra memory allocations. 

Key Features: 

  • Memory Efficiency: They provide a view over existing data, reducing memory usage. 
  • Performance: They allow fast, efficient memory access without creating new arrays. 
  • Safety: They prevent accessing out-of-bound elements, reducing errors. 

Differences Between Span<T> and ReadOnlySpan<T>: 

  • Span<T>: Mutable (you can modify data). 
  • ReadOnlySpan<T>: Immutable (data cannot be changed).

Example Code:

1. Using Span<T>:

				
					public void ModifyArray(Span<int> numbers) 
{ 
    for (int i = 0; i < numbers.Length; i++) 
    { 
        numbers[i] *= 2; 
    } 
}
				
			

// Usage 
int[] array = { 1, 2, 3 }; 
ModifyArray(array); // Modifies array elements

2. Using ReadOnlySpan:

				
					public void PrintArray(ReadOnlySpan<int> numbers) 
{ 
    foreach (var number in numbers) 
    { 
        Console.Write(number + " "); 
    } 
    Console.WriteLine(); 
}
				
			

// Usage 
int[] array = { 1, 2, 3 }; 
PrintArray(array); // Reads array elements

Creating Spans: 

  • From Arrays: Span<int> span = array; 
  • Slicing: Span<int> slice = span.Slice(1, 2); 
  • Stack Allocation: Span<int> stackSpan = stackalloc int[5];

Key Differences: Span<T>, ReadOnlySpan<T>, and Arrays

1. Memory Ownership:

  • Arrays: Own memory (allocated on the heap). 
  • Span<T> / ReadOnlySpan<T>: Don’t own memory, just provide a view of existing data. 

2. Mutability:

  • Arrays & Span<T>: Mutable (you can change elements). 
  • ReadOnlySpan<T>: Immutable (you can’t modify data).

3. Performance: 

  • Arrays: Involves overhead with copying and allocating memory. 
  • Span<T> / ReadOnlySpan<T>: Lightweight and faster, especially for temporary data and slices.

4. Flexibility:

  • Arrays: Fixed size. 
  • Span<T> / ReadOnlySpan<T>: Flexible slices from existing data.

5. Stack Allocation:

  • Arrays: Allocated on the heap. 
  • Span<T>: Can be allocated on the stack using stackalloc, which is faster for small data.

2. New lock object

What is the Lock Object in. NET 9?

Introduced in .NET 9, the Lock object simplifies thread synchronization. It provides a cleaner, more intuitive API for locking. It uses the EnterScope() method and automatically handles lock release using the Dispose() pattern.

With this, you don’t need to manually release the lock. You can just use the lock keyword as usual, and the system ensures proper lock management.

Example using Lock:

				
					Lock lockObj = new Lock(); 
lock (lockObj)  // Automatically handles locking 
{ 
    // Critical section 
}
				
			

Lock Object: Automatic, smarter, and optimized for better performance. 

Switching to the Lock object in .NET 9 simplifies your code while providing better synchronization performance.

3. New Escape Sequence

In.NET, escape sequences are used to represent special characters in strings (like newlines, tabs, or backslashes). With .NET 9, a new escape sequence for the ESCAPE character (Unicode U+001B) has been introduced, which is often used for terminal control (like text formatting or color codes).

Previous Escape Sequences

Before .NET 9, you represented the ESCAPE character using either of these:

1. Unicode Escape: \u001b

  • Example: \u001b is the ESCAPE character in Unicode.

2. Hexadecimal Escape: \x1b

  • Example: \x1b also represents the ESCAPE character, but this could be confusing if followed by more characters, like [31m (which represents red text in terminal systems).

Problem with \x1b:

If you used \x1b[31m, the parser might interpret [31m as part of the escape sequence, leading to confusion.

New Escape Sequence in .NET 9: \e

.NET 9 introduces a new, clearer escape sequence: \e.

  • \e represents the ESCAPE character (U+001B) directly. 
  • It avoids confusion with subsequent characters and is easier to read.

Example:

				
					string str = "Hello \e[31mWorld\e[0m!";
				
			
  • \e represents the ESCAPE character. 
  • [31m sets the text color to red, and [0m resets the formatting.

Why is This Better?

  • Clarity: \e clearly represents the ESCAPE character without ambiguity. 
  • No Confusion: Before .NET 9, using \x1b could cause issues if other valid characters followed it. \e eliminates this problem, making the code easier to read. 

Examples Before and After .NET 9

Before .NET 9 (C# 13 and earlier):

				
					string escapeWithUnicode = "\u001b[31mThis is red text (Before .NET 9)\u001b[0m"; 
string escapeWithHex = "\x1b[32mThis is green text (Before .NET 9)\x1b[0m";
				
			

After .NET 9:

				
					string escapeWithNewSyntax = "\e[34mThis is blue text (After .NET 9)\e[m";
				
			

4. Method Group Resolution

What is a Method Group? 

A method group in C# is a collection of methods with the same name but different parameter types. For example:

				
					class Example  
{ 
    public void Foo(int x) { } 
    public void Foo(string x) { } 
    public void Foo(double x) { } 
    public void Foo<T>(T x) { } // Generic method 
}
				
			

Here, Foo is a method group consisting of: 

  • Foo(int x) 
  • Foo(string x) 
  • Foo(double x) 
  • Foo<T>(T x) (a generic method)

You can refer to the entire method group like this:

Example example = new Example(); 
example.Foo;  // This refers to the Foo method group

What is Overload Resolution? 

Overload resolution is the process by which the compiler selects the correct method from a method group based on the arguments you pass. The compiler checks the types and other factors to find the best match. 

Before .NET 9, overload resolution involved examining all methods in the method group, which could be inefficient and problematic, especially with generics or methods with constraints.

Old Behavior: Full Candidate Set Construction

Before .NET 9, the compiler would consider every method in the group, including those that didn’t match the arguments. For example:

  • Generic methods (like Foo<T>(T x)) would be considered even if the argument type didn’t match. 
  • Methods with constraints (e.g., where T : struct) would be considered even if the argument didn’t satisfy the constraint.

This resulted in unnecessary checks, especially for methods that didn’t apply, leading to slower compilation and increased memory usage.

New Behavior in .NET 9: Pruned Candidate Set

.NET 9 optimizes this by pruning (removing) irrelevant methods early in the process. The compiler now only considers methods that could actually match the arguments.

  • Pruning Methods Early: If a generic method doesn’t match the argument type or constraints, it’s immediately removed from the candidate set. 
  • Only Valid Candidates: The compiler then only considers valid methods, which improves performance and reduces errors.

For example, if you call Foo(10) and the method group includes Foo<T>(T x) with a where T : struct constraint, the compiler immediately removes the generic method from consideration because int is a valid value type.

Key Differences Between Old and New Behavior

Aspect 

Old Behavior (Pre-.NET 9) 

New Behavior (.NET 9 and onward) 

Candidate Set 

Builds a full set of candidate methods, including irrelevant ones. 

Prunes irrelevant methods early. 

Generic Methods 

Considers all generic methods, even if parameters don’t match. 

Prunes non-matching generic methods immediately. 

Performance 

Slower due to unnecessary checks. 

Faster as irrelevant methods are removed early. 

Scope Checking 

Checks all methods globally. 

Prunes non-matching methods at each scope. 

Error Handling 

Potential for more errors due to incorrect method matching. 

Fewer errors due to more accurate matching. 

Example of the Difference

Consider the following methods: 

				
					public void Foo(int x) { } 
public void Foo<T>(T x) where T : struct { }
				
			
  • Old Behavior: Calling Foo(10) would consider both methods. The compiler would first try the generic method, but fail when it checks the constraint (T : struct), leading to unnecessary checks. 
  • New Behavior: The compiler immediately prunes the generic method, as T must be a struct and 10 is an int, which fits the non-generic method. Only Foo(int x) is considered, making the process faster.

Why This Change Matters 

  • Efficiency: By removing irrelevant methods early, the compiler spends less time checking methods that cannot match. 
  • Accuracy: The compiler only considers valid methods, reducing the chances of errors. 
  • Consistency: The new approach aligns with the general overload resolution  process, making the compiler’s behavior more predictable.

5. Implicit Index Access with the ^ Operator in C#

What is the ^ Operator (From-the-End Indexing)? 

The ^ operator in C# allows you to access elements from the end of a collection (like arrays or lists). Introduced in C# 8.0, it simplifies indexing when you want to reference the last few elements without manually calculating their indices. 

  • arr[^1] gives the last element. 
  • arr[^2] gives the second-to-last element. 
  • arr[^3] gives the third-to-last element, and so on.

Traditional Indexing

Traditionally, indexing starts from the beginning of the collection with zero-based indexes. For example:

				
					int[] arr = { 1, 2, 3, 4, 5 }; 
Console.WriteLine(arr[0]); // Prints 1 (first element) 
Console.WriteLine(arr[4]); // Prints 5 (last element)
				
			

To access the last element, you’d need to use arr.Length – 1. 

Using the ^ Operator (From the End Indexing) 

The ^ operator simplifies access to elements from the end: 

Example Code:

				
					int[] arr = { 1, 2, 3, 4, 5 }; 
Console.WriteLine(arr[^1]); // Prints 5 (last element) 
Console.WriteLine(arr[^2]); // Prints 4 (second-to-last element) 
Console.WriteLine(arr[^3]); // Prints 3 (third-to-last element)
				
			

New in C# 13: Using ^ in Object Initializers 

C# 13 introduces the ability to use the ^ operator in object initializers. This allows you to directly reference and modify array elements from the end while initializing objects, making your code more intuitive and readable. 

What is an Object Initializer?

An object initializer lets you set the properties of an object when it’s created, without needing to call a constructor for each property.

For Example:

				
					public class TimerRemaining 
{ 
    public int[] buffer { get; set; } = new int[10]; 
}
				
			

Without using ^, you would initialize the array like this:

				
					var countdown = new TimerRemaining() 
{ 
buffer = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 } 
};
				
			

Using ^ in Object Initializers (C# 13)

With C# 13, you can initialize the array starting from the end using the ^ operator. For example:

				
					var countdown = new TimerRemaining() 
{ 
    buffer =  
    { 
        [^1] = 0,  
        [^2] = 1,  
        [^3] = 2,  
        [^4] = 3,  
        [^5] = 4,  
        [^6] = 5,  
        [^7] = 6,  
        [^8] = 7,  
        [^9] = 8,  
        [^10] = 9 
	} 
};
				
			

This sets the array elements from the last index and counts backwards, improving readability.

Before vs After C# 13

Before C# 13 (Traditional Initialization):

				
					var countdown = new TimerRemaining() 
{ 
    buffer = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 } 
};
				
			

After C# 13 (Using ^ in Initializers):

				
					var countdown = new TimerRemaining() 
{ 
    	buffer =  
    	{ 
        [^1] = 0, 
        [^2] = 1, 
        [^3] = 2, 
        [^4] = 3, 
        [^5] = 4, 
        [^6] = 5, 
        [^7] = 6, 
        [^8] = 7, 
        [^9] = 8, 
        [^10] = 9 
    } 
};
				
			

Why Is This Useful?

  • Simplified Syntax: The ^ operator allows easier access to elements from the end of an array, avoiding the need to manually calculate the index (e.g., arr.Length – 1). 
  • Intuitive Initialization: When initializing arrays in reverse order or modifying the last few elements, ^ makes the code cleaner and more understandable. 
  • Cleaner Code: You no longer need complex logic to calculate indices when referencing elements from the end.

6. Using ref and unsafe in Async and Iterator Methods

C# 13 introduces significant updates for working with ref variables, ref struct types, and unsafe code in async and iterator methods. These changes make it easier to handle low-level memory management while ensuring safety.

Key Concepts

1. ref Variables and ref struct Types:

  • A ref variable holds a reference to another variable, allowing direct modifications without copying.
  • A ref struct is a type like Span<T> or ReadOnlySpan<T>, designed for memory safety and performance. They must reside on the stack and cannot be boxed or stored on the heap. 

2. Iterator Methods: 

  • Methods using yield return and yield break return values lazily, generating elements one at a time, which saves memory.

3. Async Methods:

  • async methods enable asynchronous programming using async and await. They return a Task or Task<T> and allow non-blocking operations.

4. Unsafe Code:

  • unsafe code allows direct memory manipulation, using pointers and bypassing runtime safety checks. It’s used for performance-critical tasks but can introduce bugs like memory corruption.

Before C# 13: Limitations

Prior to C# 13:

  • Async Methods: You couldn’t use ref variables or ref struct types (like Span<T>) inside async methods because they could cause stack safety issues. 
  • Iterator Methods: You couldn’t use ref variables, ref struct types, or unsafe code in iterator methods.

These restrictions existed to ensure stack safety, as async and iterator methods may involve context-switching and resuming execution, which could lead to memory issues if not handled carefully.

New Features in C# 13 

C# 13 relaxes these restrictions, allowing more flexibility while maintaining memory safety.

1. Async Methods with ref Variables and ref struct Types: 

  • You can now declare ref variables and use ref struct types like Span<T> in async methods. However, you cannot access these types across await boundaries to avoid violating stack safety.

Example:

				
					public async Task ExampleAsync() 
{ 
    Span<int> span = new Span<int>(new int[] { 1, 2, 3, 4 }); 
    ref int value = ref span[2];  // Declaring a ref variable 
    value = 10;  // Modify the value 
    await Task.Delay(1000);  // Simulate async operation 
}
				
			

In this example, the Span<int> is used inside an async method, and ref is safely modified, but you cannot access it after the await statement.

2. Iterator Methods with unsafe Code:

Iterator methods can now include unsafe code, enabling direct memory manipulation with pointers. However, yield return and yield break must stay within a safe context to prevent unsafe memory issues.

Example Code:

				
					public unsafe IEnumerable<int> GetNumbers() 
{ 
    int* ptr = stackalloc int[10];  // Unsafe code in iterator method 

    for (int i = 0; i < 10; i++) 
    { 
        ptr[i] = i; 
        yield return ptr[i];  // Yield return is safe 
    } 
}
				
			

Here, you can safely use unsafe code to work with pointers inside an iterator method, but yield return keeps the operation safe.

Benefits of the New Features 

  • Improved Performance: You can now use ref struct types like Span<T> and ReadOnlySpan<T> in async methods, allowing high-performance memory operations without heap allocations. 
  • Flexible unsafe Code in Iterators: Iterator methods can now safely use pointers, which is useful for tasks requiring direct memory access, such as low-level data processing. 
  • Safety Enforcement: The compiler ensures that ref types aren’t used across await or yield return boundaries, preserving memory safety.

7. The field Keyword in C# 13: A Simplified Approach to Property Backing Fields

This allows you to access the compiler-generated backing field of a property directly in its get and set accessors, eliminating the need to manually declare the backing field. This makes your code cleaner and reduces boilerplate. 

What is a Backing Field? 

In C#, a backing field is an automatically generated variable that stores the value of a property. Typically, when you define a property, the compiler generates a private field to hold its value.

Example:

				
					public class Person 
{ 
    private string _name; // Backing field 
 
    public string Name 
    { 
        get { return _name; }  // Access the backing field 
        set { _name = value; } // Modify the backing field 
    } 
}
				
			

How the field Keyword Works

With C# 13, you can use the field keyword to refer to this automatically generated backing field without explicitly declaring it. The compiler will handle the backing field for you.

Example with field:

				
					public class Person 
{ 
    public string Name 
    { 
        get => field;    // Access the backing field 
        set => field = value;  // Modify the backing field 
    } 
}
				
			

In this code, the compiler automatically creates the backing field for Name, and you use field to access or modify it in the get/set accessors.

What Happens Behind the Scenes? 

The compiler generates a backing field with a name like <Name>k__BackingField and uses it in the property’s get and set methods.

Example:

				
					private string <Name>k__BackingField; 
 
public string Name 
{ 
    get => <Name>k__BackingField; 
    set => <Name>k__BackingField = value; 
}
				
			

Benefits of Using field

  • Cleaner Code: No need to manually declare backing fields.
  • Less Boilerplate: Reduces the amount of code, making property definitions more concise.
  • Focus on Logic: You can focus on the logic of the property itself, without worrying about the underlying implementation.

Potential Issues to Watch Out For 

Naming Conflicts: If you already have a field or parameter named field, it will cause ambiguity. You can resolve this by:

  • Using @ to escape the keyword:

get => @field;

  • Using this to explicitly refer to the class field:

get => this.field;

Example with Conflict Resolution

				
					public class Person 
{ 
    private string field;  // A regular field 
    public string Name 
    { 
        get => @field;   // Disambiguates with the @ symbol 
        set => @field = value; 
    } 
}
				
			

8. Overload resolution priority

C# 13 introduces the OverloadResolutionPriorityAttribute, a feature designed primarily for library authors. It allows developers to specify which method overload should be preferred when there are multiple options. This helps ensure that more efficient or updated overloads are used without breaking existing code.

The Problem It Solves

As libraries evolve, new overloads may be added to improve performance or provide better functionality. However, when multiple overloads match the same method call, it can cause ambiguity, making it difficult for the compiler to decide which one to use.

Example with Conflict Resolution

				
					public class Calculator 
{ 
    public int Add(int a, int b) { return a + b; } 
    public double Add(double a, double b) { return a + b; } 
    public int Add(int a, int b, int c) { return a + b + c; } 
}
				
			

Now, suppose a more efficient overload is added:

public int Add(long a, long b) { return (int)(a + b); } 

The compiler might still prefer the older Add(int, int) method, even though the Add(long, long) method is more efficient for certain data types.

What is the OverloadResolutionPriority Attribute?

The OverloadResolutionPriority Attribute is an attribute that library authors can use to mark overloads with a priority value. Overloads with higher priority values are preferred when the compiler resolves method calls with multiple valid candidates.

How It Works

You apply the OverloadResolutionPriority Attribute to method overloads, specifying a numeric priority. Overloads with higher values are selected over those with lower values, without breaking existing code.

Example Code:

				
					public class Calculator 
{ 
    // Old method 
           //Default Priority – 0 (Least) 
    public int Add(int a, int b) { return a + b; } 
    // New, more efficient overload 
    [OverloadResolutionPriority(2)] 
    public int Add(long a, long b) { return (int)(a + b); } 
    // Another overload 
    [OverloadResolutionPriority(1)] 
    public int Add(int a, int b, int c) { return a + b + c; } 
}
				
			

In this example:

  • The Add(long, long) overload has a priority of 2, making it preferred for calls with long arguments. 
  • The Add(int, int, int) overload has a lower priority of 1. 

If there’s ambiguity (e.g., when calling Add(5L, 10L)), the compiler will prefer Add(long, long) because it has a higher priority.

Key Benefits

  • Preserve Backward Compatibility: New, optimized overloads can be added without breaking existing code. 
  • No Breaking Changes: Users don’t need to update their code unless they want to explicitly use a new overload. 
  • Disambiguation: In complex scenarios, you can guide the compiler to select the best overload.

Example in a Library

				
					public class Library 
{ 
    // Older overload 
    public string FormatMessage(string message) => "Message: " + message; 
    // New, more efficient overload with higher priority 
    [OverloadResolutionPriority(2)] 
    public string FormatMessage(StringBuilder message) => "Message: " + message.ToString(); 
    // Another overload 
    [OverloadResolutionPriority(1)] 
    public string FormatMessage(int count) => "Message repeated " + count + " times."; 
}
				
			
  • For FormatMessage(“Hello”), the compiler will prefer the FormatMessage(string) overload. 
  • For FormatMessage(new StringBuilder(“Hello”)), the FormatMessage(StringBuilder) overload will be preferred. 
  • For FormatMessage(3), the FormatMessage(int) overload will be used.

Potential Pitfalls

  • Overuse of Priority: Too many overloads with priorities can create confusion. Use this feature sparingly. 
  • Backward Compatibility: Raising the priority of an existing overload too much could cause unexpected behavior for users. 
  • Ambiguities: This attribute doesn’t resolve all overload conflicts, especially when overloads are incompatible with the arguments passed.

In C# 13, a new feature called “allows ref struct” was introduced, which changes how generics can handle “ref struct” types. Let’s break this down in simpler terms.

9. What is a Ref Struct?

A ref struct is a special type in C# that is allocated on the stack (not the heap). This makes it safer when working with raw pointers or references, ensuring that memory is used correctly. Span<T> and ReadOnlySpan<T> are two common examples of ref struct types, often used for handling chunks of memory efficiently without copying data. However, ref structs come with certain rules: 

  • They can’t be used with certain operations that require heap allocation, like in asynchronous methods or boxed into an object.
  • They are strictly tied to the memory they’re allocated in, meaning their lifetime is very specific to where they are used.

The Problem Before C# 13

Before C# 13, you couldn’t use ref struct types like Span<T> in generics. This was because ref structs have limitations on how and where they can be used, mainly because of memory safety concerns. For example, you couldn’t do something like this:

				
					public class MyClass<T>  
{ 
    T value; 
}
				
			

You couldn’t use a ref struct (like Span<T>) as the type T in this class.

The New “allows ref struct” Feature in C# 13

With C# 13, a new feature called “allows ref struct” was introduced. This feature allows generics to accept ref struct types as type parameters while still ensuring that memory safety rules are followed. It’s an anti-constraint because it actively allows what was previously disallowed.

How Does It Work?

In C# 13, when you write a generic class or method, you can now explicitly allow a ref struct type by using the allows ref struct keyword.

Here’s how it looks:

				
					public class MyClass<T> where T : allows ref struct 
{ 
    public void SomeMethod(scoped T p) 
    { 
        // Do something with p, which is a ref struct 
    } 
}
				
			

Key Points:

  • where T : allows ref struct: This line tells the compiler that T can be a ref struct. 
  • scoped T p: The scoped keyword ensures that the ref struct is only valid within a limited scope, which is another memory safety rule for ref structs.

Example Usage

Let’s say we want to write a generic class that works with ref struct types like Span<T>: 

				
					public class BufferProcessor<T> where T : allows ref struct 
{ 
    public void ProcessBuffer(scoped T buffer) 
    { 
        // Safely work with the buffer (e.g., Span<T> or ReadOnlySpan<T>) 
    } 
}
				
			

Now, T can be any ref struct type, such as Span<T>, and the ProcessBuffer method can safely operate on it while respecting all safety rules.

Benefits of “allows ref struct”

  • Memory Safety: The ref struct types come with rules that prevent unsafe memory usage (like avoiding heap allocations). The allows ref struct feature ensures these rules are still followed even in generics. 
  • Flexibility with Generics: Before C# 13, it was hard to use ref structs in generic types. Now, you can write more flexible, reusable code that works with stack-allocated types like Span<T>. 
  • Compiler Enforcement: The compiler ensures that any generic code that uses ref struct types follows all the memory safety rules. This helps reduce the chance of bugs or memory errors.

10. Introduction to Partial Members in C# 13

In C# 13, two new features called partial properties and partial indexers were introduced. These features build on the idea of partial methods and allow developers to split the implementation of properties or indexers into different parts of a class. This helps organize code, especially in large projects or situations where code is auto-generated by tools.

With these new features, you can separate the declaration and implementation of properties and indexers into different files. This is especially helpful when some parts of the code are auto generated (e.g., by a tool), and other parts are manually written by the developer.

What Are Partial Properties and Indexers?

A partial property allows you to separate its declaration (just the signature) from its implementation (the actual code that defines how the property works). The same concept applies to partial indexers.

Key Points:

  • Declaration: This part defines the signature of the property or indexer but does not include any implementation. It can specify the property name and whether it has a getter or setter.
  • Implementation: This part provides the actual behavior of the property or indexer (i.e., what happens when the getter or setter is called).

How Partial Properties Work

Declaring a Partial Property 

The declaration of a partial property defines its signature but does not include any logic for getting or setting the value. This declaration could be in one file.

Example of declaring a partial property:

				
					public partial class C 
{ 
    public partial string Name { get; set; } 
}
				
			

Here, Name is declared, but no implementation is provided. This declaration could exist in one file, while the implementation is provided in another.

Implementing a Partial Property

In another part of the class, the actual implementation of the property is provided, including the logic for the getter and setter.

Example of implementing a partial property:

				
					public partial class C 
{ 
    private string _name; 
public partial string Name 
    { 
        get => _name; 
        set => _name = value; 
    } 
}
				
			

Here, the Name property is implemented, with a backing field _name to store the value. The get and set methods define how the property behaves.

Restrictions on Partial Properties

1. No Auto-Properties in Implementation: In the implementation part, you cannot use an auto-property (like get; set;). The implementation must explicitly define how the getter and setter work.

Invalid Example:

				
					public partial class C 
{ 
    // Invalid: cannot use auto-property in the implementation 
    public partial string Name { get; set; } 
}
				
			

2. Signature Matching: The declaration and implementation of the property must have the same signature (name, type, accessors). If they don’t match, a compile-time error will occur. 

3. Private Fields: The implementation part often uses a private backing field (e.g., _name), but this is not required in the declaration.

Example of Full Partial Property

Here’s how the declaration and implementation of a partial property would look across two files.

File 1: C.Declaring.cs

				
					public partial class C 
{ 
    public partial string Name { get; set; } 
}
				
			

File 2: C.Implementing.cs

				
					public partial class C 
{ 
    private string _name; 
    public partial string Name 
    { 
        get => _name; 
        set => _name = value; 
    } 
}
				
			

In the first file (C.Declaring.cs), we declare the Name property but don’t provide any logic. In the second file (C.Implementing.cs), we provide the actual logic for the property, including the backing field _name and the getter and setter methods.

Partial Indexers

Partial indexers work in the same way as partial properties. You can split the declaration and implementation of an indexer across multiple files.

Declaring a Partial Indexer:

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

Implementing a Partial Indexer:

				
					public partial class C 
{ 
    private string[] _values = new string[10]; 
    public partial string this[int index] 
    { 
        get => _values[index]; 
        set => _values[index] = value; 
    } 
}
				
			

Here, the indexer is declared in one part of the class, and the actual logic (getting and setting values in an array) is provided in another part.

Advantages of Partial Properties and Indexers

  • Separation of Concerns: By splitting the code into multiple files, you can keep things organized and modular. This is especially helpful in large projects. 
  • Collaboration: Different developers can work on different parts of the class without conflicts. For example, one developer can handle the declaration, while another handles the implementation. 
  • Auto-Generated Code: If part of your code is generated automatically (for example, by a designer tool), you can have the tool generate the declarations, while you manually implement the logic.

11. What Changed in C# 13: Ref Struct Types Can Now Implement Interfaces

In C# 13, a significant change was introduced that allows ref struct types to implement interfaces. Before this version, ref struct types (like Span<T>, ReadOnlySpan<T>, etc.) were not allowed to implement interfaces. This was because they are designed to be allocated on the stack, and allowing them to implement interfaces could lead to potential memory safety issues. However, starting in C# 13, ref structs can now implement interfaces, which opens up new possibilities for flexible and reusable code. Despite this change, there are still some important rules to make sure that these types remain safe and efficient.

What is a ref struct?

A ref struct is a type that is specifically designed to be allocated on the stack rather than the heap. This makes them ideal for scenarios that require high performance and low memory overhead, such as working with buffers of data.

Common examples of ref structs include:

  • Span<T>: Represents a segment of an array or memory block. 
  • ReadOnlySpan<T>: A read-only version of Span<T>. 

Key points about ref structs:

  • No boxing: They cannot be converted to object, which would involve heap allocation. 
  • No async methods: They can’t be used in async methods because async methods require heap allocation. 
  • No class or struct fields: They can’t be fields in a regular class unless that class is also a ref struct.

These types are constrained to the stack to ensure memory safety and avoid potential issues like heap allocations or dangling references.

What Changed in C# 13?

In C# 13, ref structs can now implement interfaces. This allows them to participate in more flexible and reusable designs. However, to maintain their strict memory safety, there are still some important restrictions.

Key Rules and Restrictions for ref struct Types Implementing Interfaces

1. No Boxing to Interface Type: Even though ref structs can implement interfaces, they cannot be converted to the interface type. Boxing, which involves converting a value type to object, would involve heap allocation, which breaks the stack-only rule of ref structs.

Example of invalid code:

				
					public ref struct MySpan 
{ 
    public int[] Data; 
    public MySpan(int[] data) { Data = data; } 
} 
public interface IMyInterface 
{ 
    void DoSomething(); 
}
public class Test 
{ 
    public void Example() 
    { 
        MySpan span = new MySpan(new int[] { 1, 2, 3 }); 
        IMyInterface myInterface = span; // Error: Cannot box a ref struct to an interface 
    } 
}
				
			

2. No Explicit Interface Implementation: A ref struct cannot implement an interface method explicitly. Explicit implementation means the method can only be accessed through the interface type, not directly through the struct. This could lead to patterns that break memory safety. 

Example of invalid code:

				
					public ref struct MyRefStruct : IMyInterface 
{ 
    void IMyInterface.DoSomething() // Invalid for ref structs 
    { 
        Console.WriteLine("Doing something!"); 
    } 
}
				
			

3. Implementing All Interface Methods: If a ref struct implements an interface, it must implement all the methods defined in that interface. Even if some methods in the interface have default implementations, the ref struct must still provide its own implementation. This ensures that the ref struct remains in control of its behavior and adheres to the stack-based memory model.

Example of implementing an interface with default methods:

				
					public interface IMyInterface 
{ 
    void DoSomething() // Default implementation 
    { 
        Console.WriteLine("Doing something in the interface!"); 
    } 
    void DoSomethingElse(); 
} 
public ref struct MyRefStruct : IMyInterface 
{ 
    public void DoSomething() 
    { 
        Console.WriteLine("MyRefStruct does something!"); 
    } 
    public void DoSomethingElse() 
    {
        Console.WriteLine("MyRefStruct does something else!"); 
    } 
}
				
			

4. No Virtual Methods in ref struct: Ref structs cannot have virtual methods. Virtual methods require the type to be heap-allocated to support polymorphism, which goes against the design of ref structs that need to stay on the stack.

Example of invalid code:

				
					public ref struct MyRefStruct 
{ 
    // This is invalid: ref structs cannot have virtual methods 
    public virtual void MyMethod() 
    { 
        Console.WriteLine("MyMethod"); 
    } 
}
				
			

Example of a ref struct Implementing an Interface

				
					public interface IShape 
{ 
    void Draw(); 
} 
public ref struct Circle : IShape 
{ 
    private double radius; 
    public Circle(double radius) 
    { 
        this.radius = radius; 
    }
    public void Draw() 
    { 
        Console.WriteLine($"Drawing a circle with radius {radius}"); 
    } 
}
public class Test 
{ 
    public void Run() 
    { 
        Circle circle = new Circle(5.0); 
        IShape shape = circle; // Valid: ref struct can implement an interface 
        shape.Draw();  // Output: Drawing a circle with radius 5 
    }
}
				
			

In this example:

  • Circle is a ref struct that implements the IShape interface. 
  • The Draw method is implemented in the ref struct, and it can be used through the interface (IShape). 
  • No boxing or heap allocation happens, which maintains the ref struct’s stack-based memory model.

Leave a Reply

Your email address will not be published. Required fields are marked *