Skip to main content
Redhat Developers  Logo
  • Products

    Platforms

    • Red Hat Enterprise Linux
      Red Hat Enterprise Linux Icon
    • Red Hat AI
      Red Hat AI
    • Red Hat OpenShift
      Openshift icon
    • Red Hat Ansible Automation Platform
      Ansible icon
    • View All Red Hat Products

    Featured

    • Red Hat build of OpenJDK
    • Red Hat Developer Hub
    • Red Hat JBoss Enterprise Application Platform
    • Red Hat OpenShift Dev Spaces
    • Red Hat OpenShift Local
    • Red Hat Developer Sandbox

      Try Red Hat products and technologies without setup or configuration fees for 30 days with this shared Openshift and Kubernetes cluster.
    • Try at no cost
  • Technologies

    Featured

    • AI/ML
      AI/ML Icon
    • Linux
      Linux Icon
    • Kubernetes
      Cloud icon
    • Automation
      Automation Icon showing arrows moving in a circle around a gear
    • View All Technologies
    • Programming Languages & Frameworks

      • Java
      • Python
      • JavaScript
    • System Design & Architecture

      • Red Hat architecture and design patterns
      • Microservices
      • Event-Driven Architecture
      • Databases
    • Developer Productivity

      • Developer productivity
      • Developer Tools
      • GitOps
    • Automated Data Processing

      • AI/ML
      • Data Science
      • Apache Kafka on Kubernetes
    • Platform Engineering

      • DevOps
      • DevSecOps
      • Ansible automation for applications and services
    • Secure Development & Architectures

      • Security
      • Secure coding
  • Learn

    Featured

    • Kubernetes & Cloud Native
      Openshift icon
    • Linux
      Rhel icon
    • Automation
      Ansible cloud icon
    • AI/ML
      AI/ML Icon
    • View All Learning Resources

    E-Books

    • GitOps Cookbook
    • Podman in Action
    • Kubernetes Operators
    • The Path to GitOps
    • View All E-books

    Cheat Sheets

    • Linux Commands
    • Bash Commands
    • Git
    • systemd Commands
    • View All Cheat Sheets

    Documentation

    • Product Documentation
    • API Catalog
    • Legacy Documentation
  • Developer Sandbox

    Developer Sandbox

    • Access Red Hat’s products and technologies without setup or configuration, and start developing quicker than ever before with our new, no-cost sandbox environments.
    • Explore Developer Sandbox

    Featured Developer Sandbox activities

    • Get started with your Developer Sandbox
    • OpenShift virtualization and application modernization using the Developer Sandbox
    • Explore all Developer Sandbox activities

    Ready to start developing apps?

    • Try at no cost
  • Blog
  • Events
  • Videos

Some more C# 12

Advanced features

April 30, 2024
Tom Deseyn
Related topics:
.NETLinux
Related products:
Red Hat Enterprise Linux

Share:

    In the previous article on C# 12, you learned about collection expressions and primary constructors. In this article, we’ll take a look at some advanced features that part of the latest C# version: inline arrays, optional params and params in lambda expressions, ref readonly parameters, aliasing any type, and the UnsafeAccessorAttribute.

    Inline arrays

    A regular C# array is a reference type that lives on the heap. Like other reference types, the garbage collector (GC) keeps track whether the array is still referenced, and it frees up memory when the array is no longer in use.

    To avoid the GC overhead in performance sensitive code, when a small array is needed that is local to a function, it can be allocated on the stack using stackalloc. Thanks to the Span<T> type introduced in .NET Core 2.1 we can use such arrays without resorting to "unsafe" code.

    int[] bufferOnHeap = new int[1024];
    Span<int> bufferOnStack = stackalloc int[128];

    C# also allows us to allocate memory for an array as part of a struct. This can be interesting for performance, and also for interop to match a native type’s layout. Before C# 12, such arrays were declared using the fixed keyword, limited to primitive numeric types, and required using unsafe code. The following code compiles and makes no issue about the illegal out-of-bound access at compile time or run time.

    Foo();
    
    unsafe void Foo() {
      MyStruct s = default;
      s.Buffer[15] = 20; // Out-of-bounds access not caught.
    }
    
    unsafe struct MyStruct {
      public fixed byte Buffer[10];
    }

    C# 12 improves the situation, and allows declaring inline arrays and accessing them in a safe way. The buffer must be declared as a struct type with a single field for the element type and an InlineArray attribute with the length. The element type is also no longer limited to primitive numeric types. When we update the previous example to C# 12, at compile time, we get an error for the out-of-bounds access.

    void Foo() {
      MyStruct s = default;
      s.Buffer[15] = 20; // CS9166: out-of-bounds access
    }
    
    struct MyStruct {
      public MyBuffer Buffer;
    }
    
    [InlineArray(10)]
    struct MyBuffer {
      private byte _element;
    }

    As shown in the example, the buffer type supports indexing using an int. Indexing using an Index or Range type also works.

    The buffer type also converts implicity to Span<T> and ReadOnlySpan<T>, and it can also be used in a foreach.

    You can add members to the buffer type that operate on the stored data.

    Optional parameters and params in lambda expressions

    C# 12 allows lambda expressions to have default parameters as shown in the next example.

    var prefixer = (string value, string prefix = "_")
                      => $"{prefix}{value}";
    
    Console.WriteLine(prefixer("name"));
    Console.WriteLine(prefixer("name", "$"));

    We’ve used the var keyword as the target type of the lambda expression. Under the hood, the compiler will define a delegate type that stores the optional parameter values as shown in this expanded example.

    // The optional values are captured in the delegate type.
    delegate string PrefixerDelegate(string value, string prefixer = "_");
    
    PrefixerDelegate prefixer = (string value, string prefix)
                                   => $"{prefix}{value}";

    C# 12 also allows to use params in a lambda expressions.

    var adder = (params int[] numbers)
                    => numbers.Sum();
    
    int sum = adder(1, 2, 3);

    Similar to the optional parameters, the params is captured in the delegate type.

    Ref readonly parameters

    C# 7.2 introduced the in keyword which enables passing a value by reference while not allowing the value to be modified.

    MyStruct s = new MyStruct { I = 1 };
    
    Foo(s); // or: Foo(in s);
    
    void Foo(in MyStruct value) {
      value.I = 10; // CS8332: value is readonly
    }
    
    struct MyStruct {
      public int I;
    }

    As shown in the previous example, the caller is not required to use the in keyword when passing the variable. in arguments are also not limited to passing variables. As shown in the following example, we can pass temporary values that are not in scope before/after the call.

    Foo(Bar());
    Foo(default(MyStruct));
    
    MyStruct Bar() { .. }

    C# 12 introduces passing values as ref readonly. In contrast to in, the caller is required to specify the ref keyword. This means the latter examples are no longer allowed because the temporary values passed in are not referenceable. This allows us to better capture the semantics of some APIs, like when calling ReadOnlySpan(ref readonly T reference) as shown in the next example.

    MyStruct value = default;
    
    // Calling ReadOnlySpan(ref readonly T reference)
    // allows passing a referenceable value:
    var span = new ReadOnlySpan<MyStruct>(ref value);
    span[0] = ..; // operates on the referenced value
    
    // and disallows passing a non-referenceable value:
    var span = new ReadOnlySpan<int>(ref CreateMyStruct()); // CS1510: ref must be an assignable variable
    
    MyStruct CreateMyStruct() => default;
    
    struct MyStruct
    { }

    Alias any type

    C# type aliases were restricted to using the full type names:

    using Int = System.Int32;
    using TupleOfInts = System.ValueTuple<int, int>;

    While C# 12 allows us to use any C# type declarations:

    using Int = int;
    using TupleOfInts = (int, int);
    using unsafe Pointer = int*;

    UnsafeAccessorAttribute

    Serializers require access to inaccessible members of types. Previously this was only achievable using reflection. .NET 8 is introducing the UnsafeAccessorAttribute which allows to do this without using reflection. This improves performance, enables source-generators to access these members, and it works well with NativeAOT.

    The inaccessible members are made accessible by declaring an extern method declaration with an appropriate signature and adding the UnsafeAccessorAttribute to identify the member. The runtime will provide the implementation for these methods. If the member is not found, calling the method will throw MissingFieldException or MissingMethodException.

    The following example shows calling a private constructor, and calling a private property getter.

    using System.Runtime.CompilerServices;
    
    MyClass instance = Ctor(1);
    int value = GetPrivateProperty(instance);
    
    [UnsafeAccessor(UnsafeAccessorKind.Constructor)]
    extern static MyClass Ctor(int i);
    
    [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_PrivateProperty")]
    extern static int GetPrivateProperty(MyClass c);
    
    public class MyClass {
       MyClass(int i) { PrivateProperty = i; }
       int PrivateProperty { get ; }
    }

    The UnsafeAccessorAttribute documentation provides a full overview on how to access different members. Support for generic parameters is added as part of .NET 9.

    Conclusion

    In this second and final article on C# 12, we looked at inline arrays, optional params and params in lambda expressions, ref readonly parameters, aliasing any type, and the UnsafeAccessorAttribute. These new features improve C# for specific use cases.

    Related Posts

    • C# 12: Collection expressions and primary constructors

    • C# 9 top-level programs and target-typed expressions

    • C# 9 pattern matching

    • Improvements to static analysis in the GCC 14 compiler

    • Three ways to containerize .NET applications on Red Hat OpenShift

    • Containerize .NET for Red Hat OpenShift: Linux containers and .NET Core

    Recent Posts

    • Cloud bursting with confidential containers on OpenShift

    • Reach native speed with MacOS llama.cpp container inference

    • A deep dive into Apache Kafka's KRaft protocol

    • Staying ahead of artificial intelligence threats

    • Strengthen privacy and security with encrypted DNS in RHEL

    Red Hat Developers logo LinkedIn YouTube Twitter Facebook

    Products

    • Red Hat Enterprise Linux
    • Red Hat OpenShift
    • Red Hat Ansible Automation Platform

    Build

    • Developer Sandbox
    • Developer Tools
    • Interactive Tutorials
    • API Catalog

    Quicklinks

    • Learning Resources
    • E-books
    • Cheat Sheets
    • Blog
    • Events
    • Newsletter

    Communicate

    • About us
    • Contact sales
    • Find a partner
    • Report a website issue
    • Site Status Dashboard
    • Report a security problem

    RED HAT DEVELOPER

    Build here. Go anywhere.

    We serve the builders. The problem solvers who create careers with code.

    Join us if you’re a developer, software engineer, web designer, front-end designer, UX designer, computer scientist, architect, tester, product manager, project manager or team lead.

    Sign me up

    Red Hat legal and privacy links

    • About Red Hat
    • Jobs
    • Events
    • Locations
    • Contact Red Hat
    • Red Hat Blog
    • Inclusion at Red Hat
    • Cool Stuff Store
    • Red Hat Summit
    © 2025 Red Hat

    Red Hat legal and privacy links

    • Privacy statement
    • Terms of use
    • All policies and guidelines
    • Digital accessibility

    Report a website issue