.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 |
9.0 | 13.0 | 2022 update 12 | Expected November 12th 2024 | No |
Release was November 8th 2021.
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.
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.
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.
.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.
.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.
.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.
.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.
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.
A bunch of new features were discussed but did not make it for the release. This includes:
Release is expected November 10th 2022.
This is a somewhat major release with several new and relevant features.
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.
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.
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.
.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.
C# 11 introduces file visibility, so C# top level types now can be:
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).
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.
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);
}
}
}
}
.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 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.
Use of System.Runtime.Serialization.Formatters.Binary.BinaryFormatter started giving errors in .NET 5 but from .NET 7 it is an error.
To use it add to csproj file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
<NoWarn>$(NoWarn);SYSLIB0011</NoWarn>
...
</PropertyGroup>
</Project>
Release was November 14th 2023.
It is a relative small update.
It is a LTS release with 3 years support.
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).
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.
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.
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.
.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.
.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.
.NET 8.0 also include improvements for AOT compilation and better support for hardware accellerated (GPU) data types.
Release is expected November 12th 2024.
It is a relative small update.
It is not a LTS release.
C# 13.0 supports params List<> in addition to params array.
C# 12.0 code:
using System;
public class Params12
{
public static void Test(params int[] v)
{
foreach(int v1 in v) Console.Write(" {0}", v1);
Console.WriteLine();
}
public static void Main(string[] args)
{
Test(1);
Test(1, 2);
Test(1, 2, 3);
}
}
C# 13.0 code:
using System;
using System.Collections.Generic;
public class Params12
{
public static void Test1(params int[] v)
{
foreach(int v1 in v) Console.Write(" {0}", v1);
Console.WriteLine();
}
public static void Test2(params List<int> v)
{
foreach(int v1 in v) Console.Write(" {0}", v1);
Console.WriteLine();
}
public static void Main(string[] args)
{
Test1(1);
Test1(1, 2);
Test1(1, 2, 3);
Test2(1);
Test2(1, 2);
Test2(1, 2, 3);
}
}
Useful but not important.
.NET 9.0 and C# 13.0 supplement the old Monitor.Enter/Exit methods and lock block with a new Lock class that provide additionals ways to do locking.
C# 12.0 code:
using System;
using System.Threading;
public class Lock12
{
public static void Test0()
{
Thread.Sleep(1000);
}
private static object lck1 = new object();
public static void Test1()
{
Monitor.Enter(lck1);
Thread.Sleep(1000);
Monitor.Exit(lck1);
}
private static object lck2 = new object();
public static void Test2()
{
lock(lck2)
{
Thread.Sleep(1000);
}
}
public static void Test(Action a)
{
DateTime t1 = DateTime.Now;
Thread[] t = new Thread[10];
for(int i = 0; i < t.Length; i++)
{
t[i] = new Thread(new ThreadStart(a));
}
for(int i = 0; i < t.Length; i++)
{
t[i].Start();
}
for(int i = 0; i < t.Length; i++)
{
t[i].Join();
}
DateTime t2 = DateTime.Now;
Console.WriteLine((t2 - t1).TotalSeconds);
}
public static void Main(string[] args)
{
Test(Test0);
Test(Test1);
Test(Test2);
}
}
C# 13.0 code:
using System;
using System.Threading;
public class Lock13
{
public static void Test0()
{
Thread.Sleep(1000);
}
private static object lck1 = new object();
public static void Test1()
{
Monitor.Enter(lck1);
Thread.Sleep(1000);
Monitor.Exit(lck1);
}
private static object lck2 = new object();
public static void Test2()
{
lock(lck2)
{
Thread.Sleep(1000);
}
}
private static Lock lck3 = new Lock();
public static void Test3()
{
lck3.Enter();
Thread.Sleep(1000);
lck3.Exit();
}
private static Lock lck4 = new Lock();
public static void Test4()
{
using(lck4.EnterScope())
{
Thread.Sleep(1000);
}
}
private static Lock lck5 = new Lock();
public static void Test5()
{
lock(lck5)
{
Thread.Sleep(1000);
}
}
public static void Test(Action a)
{
DateTime t1 = DateTime.Now;
Thread[] t = new Thread[10];
for(int i = 0; i < t.Length; i++)
{
t[i] = new Thread(new ThreadStart(a));
}
for(int i = 0; i < t.Length; i++)
{
t[i].Start();
}
for(int i = 0; i < t.Length; i++)
{
t[i].Join();
}
DateTime t2 = DateTime.Now;
Console.WriteLine((t2 - t1).TotalSeconds);
}
public static void Main(string[] args)
{
Test(Test0);
Test(Test1);
Test(Test2);
Test(Test3);
Test(Test4);
Test(Test5);
}
}
I don't see the point in adding these new ways to lock. Supposedly the implementation is very different from the old ways and *maybe* the new ways are more efficient.
.NET 9.0 adds CountBy and AggregateBy methods to LINQ to make common LINQ operations easier.
C# 12.0 code:
using System;
using System.Collections.Generic;
using System.Linq;
public class Count12
{
public class Data
{
public int Iv { get; set; }
public string Sv { get; set; }
}
public static void Main(string[] args)
{
List<Data> lst = new List<Data> { new Data { Iv = 1, Sv = "A" },
new Data { Iv = 2, Sv = "B" },
new Data { Iv = 2, Sv = "C" },
new Data { Iv = 2, Sv = "D" },
new Data { Iv = 3, Sv = "E" },
new Data { Iv = 3, Sv = "F" } };
var res = lst.GroupBy(o => o.Iv).Select(g => new { Iv = g.Key, Count = g.Count() }).OrderBy(e => -e.Count);
foreach(var e in res)
{
Console.WriteLine("{0} {1}", e.Iv, e.Count);
}
}
}
C# 13.0 code:
using System;
using System.Collections.Generic;
using System.Linq;
public class Count13
{
public class Data
{
public int Iv { get; set; }
public string Sv { get; set; }
}
public static void Main(string[] args)
{
List<Data> lst = new List<Data> { new Data { Iv = 1, Sv = "A" },
new Data { Iv = 2, Sv = "B" },
new Data { Iv = 2, Sv = "C" },
new Data { Iv = 2, Sv = "D" },
new Data { Iv = 3, Sv = "E" },
new Data { Iv = 3, Sv = "F" } };
var res = lst.CountBy(o => o.Iv).OrderBy(e => -e.Value);
foreach(var e in res)
{
Console.WriteLine("{0} {1}", e.Key, e.Value);
}
}
}
Maybe a small thing but still pretty useful. I am sure this will be used.
.NET 9.0 adds some new options (IndentSize) to JsonSerializer.
C# 12.0 code:
using System;
using System.Text.Json;
public class Json12
{
public class C
{
public int C1 { get; set; }
public string C2 { get; set; }
}
public class P
{
public int P1 { get; set; }
public string P2 { get; set; }
public C P3 { get; set; }
}
public static void Main(string[] args)
{
P o = new P { P1 = 123, P2 = "ABC", P3 = new C { C1 = 456, C2 = "DEF" } };
Console.WriteLine(JsonSerializer.Serialize(o));
Console.WriteLine(JsonSerializer.Serialize(o, new JsonSerializerOptions { WriteIndented = true } ));
}
}
C# 13.0 code:
using System;
using System.Text.Json;
public class Json13
{
public class C
{
public int C1 { get; set; }
public string C2 { get; set; }
}
public class P
{
public int P1 { get; set; }
public string P2 { get; set; }
public C P3 { get; set; }
}
public static void Main(string[] args)
{
P o = new P { P1 = 123, P2 = "ABC", P3 = new C { C1 = 456, C2 = "DEF" } };
Console.WriteLine(JsonSerializer.Serialize(o));
Console.WriteLine(JsonSerializer.Serialize(o, new JsonSerializerOptions { WriteIndented = true, IndentSize = 4 } ));
}
}
Definitely useful and it will be used, but we down in micro enhancements land here.
Besides the above there are many other changes.
Note that the extension class mechanism did not make it.
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 |
1.4 | August 25th 2024 | Add .NET 9 and C# 13 |
See list of all articles here
Please send comments to Arne Vajhøj