.NET 6.0, C# 10.0 and later New Features

Content:

  1. Version numbers
  2. .NET 6.0 and C# 10.0
    1. Release
    2. General
    3. Global using
    4. Namespace declaration
    5. Date and time
    6. Chunk
    7. PriorityQueue
    8. PeriodicTimer
    9. Other
    10. Not included
  3. .NET 7.0 and C# 11.0
    1. Release
    2. General
    3. Raw strings
    4. List patterns
    5. Required properties
    6. 128 bit integers
    7. File visibility
    8. Static interface members
    9. Generic math
    10. Tar format support
    11. Other
  4. .NET 8.0 and C# 12.0
    1. Release
    2. General
    3. Primary constructor
    4. Type alias
    5. Collection expressions and spread operator
    6. Lambda default value
    7. System.Collections.Frozen
    8. Guard methods
    9. Other

    Version numbers:

    .NET core C# language/compiler Visual Studio IDE Release date LTS
    5.0 9.0 2019 update 8 November 10th 2020 No
    6.0 10.0 2022 November 8th 2021 Yes
    7.0 11.0 2022 update 4 November 10th 2022 No
    8.0 12.0 2022 update 8 November 14th 2023 Yes

    .NET 6.0 and C# 10.0

    Release:

    Release was November 8th 2021.

    General:

    This is a relative small update. A lot of the most interesting new features did not make it for the release.

    The most important is probably that this release unlike the previous version is a LTS version with at least 3 years of full support.

    Global using:

    C# 10 introduces two new fetures to reduce the number of using directives needed:

    C# 9 code:

    using System;
    using System.Collections.Generic;
    
    public class Demo
    {
        public static void Test(List<int> lst)
        {
            foreach(int v in lst)
            {
                Console.WriteLine(v);
            }
        }
    }
    
    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            List<int> lst = new List<int> { 1, 2, 3 };
            Demo.Test(lst);
        }
    }
    

    C# 10 code:

    /*
    Actually the two global using are not needed as the following namespaces
    are implicit global using with a default csproj file:
        System
        System.Collections.Generic
        System.IO
        System.Linq
        System.Net.Http
        System.Threading
        System.Threading.Tasks
    */
    
    global using System;
    global using System.Collections.Generic;
    
    public class Demo
    {
        public static void Test(List<int> lst)
        {
            foreach(int v in lst)
            {
                Console.WriteLine(v);
            }
        }
    }
    
    public class Program
    {
        public static void Main(string[] args)
        {
            List<int> lst = new List<int> { 1, 2, 3 };
            Demo.Test(lst);
        }
    }
    

    I do not really like the feature as I think it makes the code less readable. But I expect it to be used anyway - it is easier.

    Namespace declaration:

    C# 10 introduces the ability to write a file scoped namespace at the top of a file similar to Java

    C# 9 code:

    using System;
    
    namespace NS9
    {
        public class Program
        {
           public static void Main(string[] args)
           {
               Console.WriteLine(typeof(Program).FullName);
           }
        }
    }
    

    C# 10 code:

    namespace NS10;
    
    using System;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(typeof(Program).FullName);
        }
    }
    

    I believe this is how it should have been from the beginning - multiple namespaces in one file is not good. I expect people to use the feature if they become aware, but many may miss it.

    Date and time:

    .NET 6 introduces some new types DateOnly and TimeOnly to supplement DateTime and TimeSpan types.

    C# 9 code:

    using System;
    
    namespace DT9
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                DateTime dt = DateTime.Now;
                Console.WriteLine(dt);
                DateTime donly = dt.Date;
                Console.WriteLine(donly);
                TimeSpan tonly = dt.TimeOfDay;
                Console.WriteLine(tonly);
            }
        }
    }
    

    C# 10 code:

    using System;
    
    namespace DT10
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                DateTime dt = DateTime.Now;
                Console.WriteLine(dt);
                DateOnly donly = DateOnly.FromDateTime(dt);
                Console.WriteLine(donly);
                TimeOnly tonly = TimeOnly.FromDateTime(dt);
                Console.WriteLine(tonly);
            }
        }
    }
    

    Nice feature if one need it, but not important.

    Chunk:

    .NET 6 introduces a Chunk method on IEnumerable<T> to allow for easy chunking.

    C# 9 code:

    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            List<string> lst = new List<string> { "A", "B", "C", "D", "E", "F", "G" };
            int n = 0;
            foreach(string s in lst)
            {
                n++;
                Console.Write(" {0}", s);
                if((n % 3) == 0 || n == (lst.Count - 1)) Console.WriteLine();
            }
        }
    }
    

    C# 10 code:

    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(String[] args)
        {
            List<string> lst = new List<string> { "A", "B", "C", "D", "E", "F", "G" };
            foreach(IEnumerable<string> chlst in lst.Chunk(3))
            {
                foreach(string s in chlst)
                {
                    Console.Write(" {0}", s);
                }
                Console.WriteLine();
            }
        }
    }
    

    I like this feature, but usage will obviously be limited to how often one really needs chunking.

    PriorityQueue:

    .NET 6 introduces a PriorityQueue, which is a queue where Dequeu is not based on order of Enqueue but a priority.

    Example:

    using System.Collections.Generic;
    
    namespace PrioQueue10
    {
        public class MyTask
        {
            public string? Name { get; set; }
        }
        public class Program
        {
            public static void Main(string[] args) 
            {
                PriorityQueue<MyTask,int> q = new PriorityQueue<MyTask,int>();
                q.Enqueue(new MyTask { Name = "A" }, 30);
                q.Enqueue(new MyTask { Name = "B" }, 10);
                q.Enqueue(new MyTask { Name = "C" }, 20);
                q.Enqueue(new MyTask { Name = "D" }, 50);
                q.Enqueue(new MyTask { Name = "E" }, 40);
                while(q.Count > 0)
                {
                    MyTask t = q.Dequeue();
                    Console.WriteLine(t.Name);
                }
            }
        }
    }
    

    Nice feature if one need it, but not important.

    PeriodicTimer:

    .NET 6 introduces a PeriodicTimer, which is an async/await based timer.

    Example:

    using System;
    using System.Threading;
    
    namespace TimPer10
    {
        public class Program
        {
            public async static Task Test()
            {
                PeriodicTimer t = new PeriodicTimer(TimeSpan.FromSeconds(3));
                while(await t.WaitForNextTickAsync())
                {
                    Console.WriteLine(DateTime.Now);
                }
            }
            public static void Main(string[] args)
            {
                Test();
                Console.WriteLine("Press enter to exit");
                Console.ReadKey();
            }
        }
    }
    
    

    I do not see the benefits compared to existing timers.

    Other:

    C# 10 introduces a record struct similar to C# 9 record class.

    .NET contain a bunch of optimzations in the JIT compilation that in some cases provide significant performance improvements.

    Not included:

    A bunch of new features were discussed but did not make it for the release. This includes:

    .NET 7.0 and C# 11.0:

    Release:

    Release is expected November 10th 2022.

    General:

    This is a somewhat major release with several new and relevant features.

    Raw strings:

    C# 11 introduces raw strings similar to PHP heredoc/nowdoc and Java 13+ text blocks.

    Raw strings are delimited by 3 double quotes. And first start at the next line!

    C# 10 code:

    using System;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            string s1 = "A\n" +
                        "\"BB\"\n" +
                        "CCC";
            Console.WriteLine(s1);
            string s2 = @"A
    ""BB""
    CCC";
            Console.WriteLine(s2);
        }
    }
    

    C# 11 code:

    using System;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            string s1 = "A\n" +
                        "\"BB\"\n" +
                        "CCC";
            Console.WriteLine(s1);
            string s2 = @"A
    ""BB""
    CCC";
            Console.WriteLine(s2);
            string s3 = """
    A
    "BB"
    CCC
    """;
            Console.WriteLine(s3);
        }
    }
    

    A useful feature in some cases.

    List patterns:

    C# 11 introduces the abilities to match on list patterns.

    The patterns are specified as square brackets with comma separated values.

    C# 10 code:

    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            List<int> lst = new List<int> { 2, 3 };
            if(lst.SequenceEqual(new List<int> { 1, 2 }))
            {
                Console.WriteLine("1,2");
            }
            else if(lst.SequenceEqual(new List<int> { 2, 3 }))
            {
                Console.WriteLine("2,3");
            }
            else if(lst.SequenceEqual(new List<int> { 3, 4 }))
            {
                Console.WriteLine("3,4");
            }
        }
    }
    

    C# 11 code:

    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            List<int> lst = new List<int> { 2, 3 };
            switch(lst)
            {
                case [1,2]:
                    Console.WriteLine("1,2");
                    break;
                case [2,3]:
                    Console.WriteLine("2,3");
                    break;
                case [3,4]:
                    Console.WriteLine("3,4");
                    break;
            }
        }
    }
    

    A useful feature in some cases.

    Required properties:

    C# 11 introduces the ability to require properties to be initialized.

    This is achieved by putting the required keyword on the property.

    C# 10 code with runtime error:

    using System;
    
    namespace ReqProp9
    {
        public class Quotient
        {
            public int P { get; init; }
            public int Q { get; init; }
            public override string ToString()
            {
                return string.Format("{0} / {1} = {2}", P, Q, P * 1.0 / Q);
            }
        }
        public class Program
        {
            public static void Main(string[] args)
            {
                Console.WriteLine(new Quotient { P = 2, Q = 3 });
                Console.WriteLine(new Quotient { P = 2 });
            }
        }
    }
    

    C# 11 code with compile error:

    using System;
    
    namespace ReqProp11
    {
        public class Quotient
        {
            public required int P { get; init; }
            public required int Q { get; init; }
            public override string ToString()
            {
                return string.Format("{0} / {1} = {2}", P, Q, P * 1.0 / Q);
            }
        }
        public class Program
        {
            public static void Main(string[] args)
            {
                Console.WriteLine(new Quotient { P = 2, Q = 3 });
                Console.WriteLine(new Quotient { P = 2 });
            }
        }
    }
    

    Given how common it is in C# to use initialization by properties instead of constructor aka new Classname { P1 = expr_1, ..., Pn = expr_n } instead of new Classname(expr_1, ..., expr_n) then this is a long needed feature to avoid insufficient initialization.

    128 bit integers:

    .NET 7 introduces 128 bit signed and unsigned integers - Int128 and UInt128.

    Example:

    using System;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            long v64 = 1;
            Int128 v128 = 1;
            for(int i = 1; i <= 25; i++)
            {
                v64 *= i;
                v128 *= i;
                Console.WriteLine("{0} {1} {2}", i, v64, v128);
            }
        }
    }
    

    I don't think it is particular important. If 64 bit integers are not large enough, then one probably need unlimited large integers.

    File visibility

    C# 11 introduces file visibility, so C# top level types now can be:

    public
    visible to everybody
    internal
    visible to assembly
    file
    visible to file

    Example:

    namespace Vis11
    {
        public class C1
        {
        }
        internal class C2
        {
        }
        file class C3
        {
        }
    }
    

    I don't think it is particular important. The difference between file and internal visibilty is small (zero if the source code organization is one file per assembly).

    Static interface members:

    C# 11 introduces static abstract methods in interfaces.

    Example:

    using System;
    
    public interface IX
    {
        public static abstract void M();
    }
    
    public class X1 : IX
    {
        public static void M()
        {
            Console.WriteLine("X1.M");
        }
    }
    
    public class X2 : IX
    {
        public static void M()
        {
            Console.WriteLine("X2.M");
        }
    }
    
    public class Program
    {
        public static void Test<T>(T o) where T : IX
        {
            T.M();
        }
        public static void Main(string[] args)
        {
            X1.M();
            X2.M();
            Test(new X1());
            Test(new X2());
        }
    }
    

    The usage in above example is obscure and irrelevant. But operators are technically static methods. And the ability to define operators on interfaces to be used by generic code allows for some very reusable code.

    Generic math:

    Specifically C# 11 and .NET 7 supports generic math.

    C# 10 code:

    using System;
    
    public class IntPair
    {
        public int V1 { get; set; }
        public int V2 { get; set; }
        public static IntPair operator +(IntPair o1, IntPair o2)
        {
            return new IntPair { V1 = o1.V1 + o2.V1, V2 = o1.V2 + o2.V2 };
        }
        public static IntPair operator -(IntPair o1, IntPair o2)
        {
            return new IntPair { V1 = o1.V1 - o2.V1, V2 = o1.V2 - o2.V2 };
        }
        public override string ToString() 
        {
            return $"({V1},{V2})";
        }
    }
    
    public class DoublePair
    {
        public double V1 { get; set; }
        public double V2 { get; set; }
        public static DoublePair operator +(DoublePair o1, DoublePair o2)
        {
            return new DoublePair { V1 = o1.V1 + o2.V1, V2 = o1.V2 + o2.V2 };
        }
        public static DoublePair operator -(DoublePair o1, DoublePair o2)
        {
            return new DoublePair { V1 = o1.V1 - o2.V1, V2 = o1.V2 - o2.V2 };
        }
        public override string ToString() 
        {
            return $"({V1},{V2})";
        }
    }
    
    public class Program
    {
       public static void Main(string[] args)
       {
           IntPair p1 = new IntPair { V1 = 1, V2 = 2 };
           IntPair p2 = new IntPair { V1 = 3, V2 = 4 };
           IntPair res12 = p1 + p2;
           Console.WriteLine("{0} + {1} = {2}", p1, p2, res12);
           DoublePair p3 = new DoublePair { V1 = 0.1, V2 = 0.2 };
           DoublePair p4 = new DoublePair { V1 = 0.3, V2 = 0.4 };
           DoublePair res34 = p3 + p4;
           Console.WriteLine("{0} + {1} = {2}", p3, p4, res34);
       }
    }
    

    We see a lot of repeated code.

    An attempt to make the code generic fails in C# 10 .NET 6.

    public class Pair<T>
    {
        public T V1 { get; set; }
        public T V2 { get; set; }
        public static Pair<T> operator +(Pair<T> o1, Pair<T> o2)
        {
            return new Pair<T> { V1 = o1.V1 + o2.V1, V2 = o1.V2 + o2.V2 };
        }
        public static Pair<T> operator -(Pair<T> o1, Pair<T> o2)
        {
            return new Pair<T> { V1 = o1.V1 - o2.V1, V2 = o1.V2 - o2.V2 };
        }
        public override string ToString() 
        {
            return $"({V1},{V2})";
        }
    }
    

    gives a compile error as there is no way to know if T supports + and -.

    C# 11 code:

    using System;
    using System.Numerics;
    
    public class Pair<T> where T : IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>
    {
        public required T V1 { get; set; }
        public required T V2 { get; set; }
        public static Pair<T> operator +(Pair<T> o1, Pair<T> o2)
        {
            return new Pair<T> { V1 = o1.V1 + o2.V1, V2 = o1.V2 + o2.V2 };
        }
        public static Pair<T> operator -(Pair<T> o1, Pair<T> o2)
        {
            return new Pair<T> { V1 = o1.V1 - o2.V1, V2 = o1.V2 - o2.V2 };
        }
        public override string ToString() 
        {
            return $"({V1},{V2})";
        }
    }
    
    public class Program
    {
       public static void Main(string[] args)
       {
           Pair<int> p1 = new Pair<int> { V1 = 1, V2 = 2 };
           Pair<int> p2 = new Pair<int> { V1 = 3, V2 = 4 };
           Pair<int> res12 = p1 + p2;
           Console.WriteLine("{0} + {1} = {2}", p1, p2, res12);
           Pair<double> p3 = new Pair<double> { V1 = 0.1, V2 = 0.2 };
           Pair<double> p4 = new Pair<double> { V1 = 0.3, V2 = 0.4 };
           Pair<double> res34 = p3 + p4;
           Console.WriteLine("{0} + {1} = {2}", p3, p4, res34);
       }
    }
    

    Very useful for any code needing to support similar calculation on multiple data types.

    Probably as a side effect of that then the Half type (IEEE 16 bit floating point) now comes with operator support.

    Example:

    using System;
    
    public class Program
    {
        public static void Main(string[] args)
        {
            Half x = (Half)0;
            for(int i = 0; i < 2500; i++)
            {
                x = x + (Half)1;
                if(i % 100 == 0)
                {
                    Console.WriteLine("{0} : {1}", i + 1, x);
                }
            }
        }
    }
    

    Tar format support:

    .NET 7 introduces a a TarFile class for writing and reading tar files.

    The API is very high level.

    Example (stuffing the bin directory tree into a bin.tar file):

    using System;
    using System.Formats.Tar;
    
    public class Program
    {
        public static void Main(string[] args) 
        {
            TarFile.CreateFromDirectory("bin", "bin.tar", false);
        }
    }
    

    Other:

    Other features include:

    Native AOT compilation allows for compilation to native binary at build time. It is more than just NGEN. It seems to be the .NET answer to Java's GraalVM AOT compilation used by Quarkus and other for auto-scaling container deployment.

    MAUI is a multi-platform UI based on Xamarin. Very important for those doing GUI's on multiple platforms.

    .NET 8.0 and C# 12.0:

    Release:

    Release was November 14th 2023.

    General:

    It is a relative small update.

    It is a LTS release with 3 years support.

    Primary constructor:

    C# 12 supports declaring fields in class declaration a socalled primary constructor. This is very similar to newer languages like Kotlin. And similar to record class introduced in C# 9.0.

    Syntax:

    visibility class ClassName(type1 field1, ..., typen fieldn)
    {
        ...
    }
    

    C# 11 code:

    using System;
    
    public class C
    {
        public C(int iv, double xv, string sv)
        {
            Iv = iv;
            Xv = xv;
            Sv = sv;
        }
        public C() : this(0, 0.0, "")
        {
        }
        public int Iv { get; set; }
        public double Xv { get; set; }
        public string Sv { get; set; }
        public override string ToString()
        {
            return string.Format("({0},{1},{2})", Iv, Xv, Sv);
        }
    }
    
    public class PCtor11
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(new C(123, 123.456, "ABC"));
            Console.WriteLine(new C());
        }
    }
    

    or with initializer instead of constructor:

    using System;
    
    public class C
    {
        public int Iv { get; set; } = 0;
        public double Xv { get; set; } = 0.0;
        public string Sv { get; set; } = "";
        public override string ToString()
        {
            return string.Format("({0},{1},{2})", Iv, Xv, Sv);
        }
    }
    
    public class PCtor11X
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(new C { Iv = 123, Xv = 123.456, Sv = "ABC" });
            Console.WriteLine(new C());
        }
    }
    

    C# 11 code:

    using System;
    
    public class C(int iv, double xv, string sv)
    {
        public C() : this(0, 0.0, "")
        {
        }
        public override string ToString()
        {
            return string.Format("({0},{1},{2})", iv, xv, sv);
        }
    }
    
    public class PCtor12
    {
        public static void Main(string[] args)
        {
            Console.WriteLine(new C(123, 123.456, "ABC"));
            Console.WriteLine(new C());
        }
    }
    

    Definitely a useful feature. The new syntax is way simpler than the old syntax (and the initializer even though convenient syntax leans itself to expose everything).

    Type alias:

    C# supports type aliases to make code more self-explanatory by using domain specific type aliases.

    C# 11 code:

    using System;
    
    public class Alias11
    {
        public static void Test(decimal v) 
        {
            Console.WriteLine(v);
        }
        public static void Main(string[] args)
        {
            decimal v = 123.45m;
            Test(v);
        }
    }
    

    C# 12 code:

    using System;
    
    using Amount = decimal;
    
    public class Alias12
    {
        public static void Test(Amount v) 
        {
            Console.WriteLine(v);
        }
        public static void Main(string[] args)
        {
            Amount v = 123.45m;
            Test(v);
        }
    }
    

    It may be a small feature, but I consider it useful. It is stuff like this that helps making code readable.

    Collection expressions and spread operator:

    C# 12 adds collection literals, collection expressions and spread operator.

    Arrays and lists can be initialized with a [] expression.

    An array or list can be spread out in such an expression using the .. spread operator.

    (Support for dictionaries are expected in C# 13)

    C# 11 code:

    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(string[] args) 
        {
            int[] arr1 = new int[] { 1, 2, 3 };
            int[] arr2 = new int[] { 4, 5, 6 };
            int[] arr3 = new int[arr1.Length + arr2.Length];
            Array.Copy(arr1, 0, arr3, 0, arr1.Length);
            Array.Copy(arr2, 0, arr3, arr1.Length, arr2.Length);
            foreach(int v in arr3) Console.Write(" {0}", v);
            Console.WriteLine();
            List<int> lst1 = new List<int> { 1, 2, 3 };
            List<int> lst2 = new List<int> { 4, 5, 6 };
            List<int> lst3 = new List<int>();
            lst3.AddRange(lst1);
            lst3.AddRange(lst2);
            foreach(int v in lst3) Console.Write(" {0}", v);
            Console.WriteLine();
        }
    }
    

    C# 12 code:

    using System;
    using System.Collections.Generic;
    
    public class Program
    {
        public static void Main(string[] args) 
        {
            int[] arr1 = [ 1, 2, 3 ];
            int[] arr2 = [ 4, 5, 6 ];
            int[] arr3 = [ ..arr1, ..arr2 ];
            foreach(int v in arr3) Console.Write(" {0}", v);
            Console.WriteLine();
            List<int> lst1 = [ 1, 2, 3 ];
            List<int> lst2 = [ 4, 5, 6 ];
            List<int> lst3 = [ ..lst1, ..lst2 ];
            foreach(int v in lst3) Console.Write(" {0}", v);
            Console.WriteLine();
        }
    }
    

    This is a very nice feature and I am sure it will be widely used.

    Lambda default value:

    C# 12 supports default values in lambda's.

    C# 11 code:

    using System;
    
    public class LDV11
    {
        public static void Main(string[] args)
        {
            Action<string> f1 = (string s) => { Console.WriteLine(s); };
            Action<string, int> fn = (string s, int n) => { for(int i = 0; i < n; i++) Console.WriteLine(s); };
            f1("1");
            fn("n", 3);
        }
    }
    

    C# 12 code:

    using System;
    
    public class LDV12
    {
        public static void Main(string[] args)
        {
            var f = (string s, int n = 1) => { for(int i = 0; i < n; i++) Console.WriteLine(s); };
            f("1");
            f("n", 3);
        }
    }
    

    Cute but I do not see this as anything important.

    System.Collections.Frozen:

    .NET 8 adds a new namespace System.Collections.Frozen with classes FrozenDictionary and FrozenSet.

    Frozen collection are like readonly collections, but with no way to change the underlying data using the original collection.

    Example:

    using System;
    using System.Collections.Frozen;
    using System.Collections.Generic;
    
    public class C
    {
        public int V { get; set; }
    }
    
    public class Froz12
    {
        public static void Main(string[] args)
        {
            IDictionary<string,C> d1 = new Dictionary<string,C> { ["A"] = new C { V = 123 } };
            d1.Add("B", new C { V = 456 });
            Console.WriteLine("{0} {1}", d1["A"].V, d1["B"].V);
            IDictionary<string,C> d2 = d1.AsReadOnly();
            try
            {
                d2.Add("C", new C { V = 0 });
            }
            catch(NotSupportedException)
            {
                Console.WriteLine("You can't add or remove entries in readonly dictionary");
            }
            d1.Add("C", new C { V = 0 }); // but you can add to or remove from underlying dictionary
            Console.WriteLine("{0} {1} {2}", d2["A"].V, d2["B"].V, d2.ContainsKey("C"));
            IDictionary<string,C> d3 = d1.ToFrozenDictionary();
            try
            {
                d3.Add("D", new C { V = 0 });
            }
            catch(NotSupportedException)
            {
                Console.WriteLine("You can't add or remove entries in frozen dictionary");
            }
            d1.Add("D", new C { V = 0 }); // changes to underlying dictionary are ignored
            Console.WriteLine("{0} {1} {2} {3}", d3["A"].V, d3["B"].V, d3.ContainsKey("C"), d3.ContainsKey("D"));
        }
    }
    

    I think this is something that will only be useful in very rare cases. The official justification is to allow for aggressive optimization.

    Guard methods:

    .NET 8 added more convenience methods for throwing exceptions as part of argument check (guard methods) on top of those added in .NET 6.

    Traditional argument check:

    using System;
    
    public class Guard11
    {
        public static void M1(string? v)
        {
            if(v == null)
            {
                throw new ArgumentNullException("v");
            }
        }
        public static void M2(int v)
        {
            if(v == 0)
            {
                throw new ArgumentException("v=0");
            }
            if(v < 0)
            {
                throw new ArgumentOutOfRangeException("v");
            }
        }
        public static void Test1(string? v)
        {
            try
            {
                M1(v);
                Console.WriteLine("OK");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        public static void Test2(int v)
        {
            try
            {
                M2(v);
                Console.WriteLine("OK");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        public static void Main(string[] args)
        {
            Test1("ABC");
            Test1(null);
            Test2(1);
            Test2(0);
            Test2(-1);
        }
    }
    

    .NET 8 argument check:

    using System;
    
    public class Guard11
    {
        public static void M1(string? v)
        {
            ArgumentNullException.ThrowIfNull(v, "v");
        }
        public static void M2(int v)
        {
            ArgumentOutOfRangeException.ThrowIfEqual(v, 0);
            ArgumentOutOfRangeException.ThrowIfNegative(v);
        }
        public static void Test1(string? v)
        {
            try
            {
                M1(v);
                Console.WriteLine("OK");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        public static void Test2(int v)
        {
            try
            {
                M2(v);
                Console.WriteLine("OK");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
        public static void Main(string[] args)
        {
            Test1("ABC");
            Test1(null);
            Test2(1);
            Test2(0);
            Test2(-1);
        }
    }
    

    Nice but probably not so important.

    Other:

    .NET 8.0 also include improvements for AOT compilation and better support for hardware accellerated (GPU) data types.

    Article history:

    Version Date Description
    1.0 October 10th 2021 Initial version
    1.1 November 16th 2021 Update after release
    1.2 October 11th 2022 Add .NET 7 and C# 11
    1.3 July 4th 2023 Add .NET 8 and C# 12

    Other articles:

    See list of all articles here

    Comments:

    Please send comments to Arne Vajhøj