.NET framework | .NET runtime | C# language/compiler | Visual Studio IDE | Release date |
---|---|---|---|---|
1.0 | 1.0 | 1.0 | 2002 | February 13th 2002 |
1.1 | 1.1 | 1.2 | 2003 | April 24th 2003 |
2.0 | 2.0 | 2.0 | 2005 | November 7th 2005 |
3.5 | 2.0 | 3.0 | 2008 | November 19th 2007 |
4.0 | 4.0 | 4.0 | 2010 | April 12th 2010 |
4.5 | 4.0 | 5.0 | 2012 | August 15th 2012 |
4.5.1 | 4.0 | 5.0 | 2013 | October 17th 2013 |
4.6 | 4.0 | 6.0 | 2015 | July 20th 2015 |
4.6.2 | 4.0 | 7.0 | 2017 | March 7th 2017 (framework August 2nd 2016) |
4.6.2 | 4.0 | 7.1 | 2017 update 3 | August 14th 2017 |
4.7.1 | 4.0 | 7.2 | 2017 update 5 | November 11th 2017 (framework October 17th 2017) |
4.7.2 | 4.0 | 7.3 | 2017 update 7 | May 7th 2018 (framework April 30th 2018) |
4.8 | 4.0 | 8.0 | 2019 update 3 | September 19th 2019 (framework April 18th 2019) |
.NET framework, .NET runtime, C# language/compiler and Visual Studio IDE each have their own version numbering scheme. For this release they are at 4.6.2, 4.0, 7.1 and 2017 update 3 respectively.
VS 2017 update 3 was released August 14th 2017.
This is a very small update.
In C# 7.0 it was possible to create tuples with default field names:
using System;
namespace E
{
public class Program
{
public static void Main(string[] args)
{
int a = 123;
int b = 456;
var res = (a, b);
Console.WriteLine($"{res.Item1} {res.Item2}");
}
}
}
and explict field names:
using System;
namespace E
{
public class Program
{
public static void Main(string[] args)
{
int a = 123;
int b = 456;
var res = (a: a, b: b);
Console.WriteLine($"{res.a} {res.b}");
}
}
}
In C# 7.1 the field names can be inferred:
using System;
namespace E
{
public class Program
{
public static void Main(string[] args)
{
int a = 123;
int b = 456;
var res = (a, b);
Console.WriteLine($"{res.a} {res.b}");
}
}
}
I consider this feature totally insignificant.
C# 7.1 allows:.
typename variablename = default;
instead of:
typename variablename = default(typename);
C# 7.0 code:
using System;
namespace E
{
public class C
{
}
public struct S
{
}
public class Demo<T>
{
private int iv;
private string sv;
private C c;
private S s;
private T o;
public Demo()
{
iv = default(int);
sv = default(string);
c = default(C);
s = default(S);
o = default(T);
}
private string ToString(object o)
{
switch(o)
{
case ValueType dummy:
return "valuetype";
case null:
return "null";
default:
return "reftype";
}
}
public override string ToString()
{
return String.Format("{0} {1} {2} {3} {4}", ToString(iv), ToString(sv), ToString(c), ToString(s), ToString(o));
}
}
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(new Demo<int>());
Console.WriteLine(new Demo<string>());
}
}
}
C# 7.1 code:
using System;
namespace E
{
public class C
{
}
public struct S
{
}
public class Demo<T>
{
private int iv;
private string sv;
private C c;
private S s;
private T o;
public Demo()
{
iv = default;
sv = default;
c = default;
s = default;
o = default;
}
private string ToString(object o)
{
switch(o)
{
case ValueType dummy:
return "valuetype";
case null:
return "null";
default:
return "reftype";
}
}
public override string ToString()
{
return String.Format("{0} {1} {2} {3} {4}", ToString(iv), ToString(sv), ToString(c), ToString(s), ToString(o));
}
}
public class Program
{
public static void Main(string[] args)
{
Console.WriteLine(new Demo<int>());
Console.WriteLine(new Demo<string>());
}
}
}
I consider this feature totally insignificant.
.NET framework, .NET runtime, C# language/compiler and Visual Studio IDE each have their own version numbering scheme. For this release they are at 4.7.1, 4.0, 7.2 and 2017 update 5 respectively.
VS 2017 update 5 was released December 4th 2017.
There are some major changes to both language and framework in this release.
But these major changes will only impact some not all.
I think that it will be a very small fraction of C#/.NET developers that will ever utilize the new value features.
Types have gotten a new visbility level:
It means that the type can be extended but only within the enclosing class.
Example:
namespace E
{
public class Program
{
private protected class P
{
}
private class C : P
{
}
public static void Main(string[] args)
{
C o = new C();
}
}
}
I consider this feature insignificant.
C# 7.2 has a new in keyword to supplement existing out and ref keywords for calls.
Example:
using System;
namespace E
{
public struct S
{
public int A;
public int B;
public int C;
public S(int a, int b, int c)
{
A = a;
B = b;
C = c;
}
public override string ToString()
{
return string.Format("({0},{1},{2})", A, B, C);
}
}
public class Program
{
public static void M(S v1, ref S v2, out S v3, in S v4)
{
v1.A = 321;
v2.A = 321;
// requuired:
v3 = new S();
v3.A = 321;
// not allowed:
//v4.A = 321;
}
public static void Main(string[] args)
{
S v1 = new S(123, 456, 789);
S v2 = new S(123, 456, 789);
S v3 = new S(123, 456, 789);
S v4 = new S(123, 456, 789);
Console.WriteLine("{0} {1} {2} {3}", v1, v2, v3, v4);
M(v1, ref v2, out v3, v4);
Console.WriteLine("{0} {1} {2} {3}", v1, v2, v3, v4);
}
}
}
Output:
(123,456,789) (123,456,789) (123,456,789) (123,456,789) (123,456,789) (321,456,789) (321,0,0) (123,456,789)
Comparison:
keyword | modifiable | mandatory assignment | changes seen outside | keyword required in call |
---|---|---|---|---|
yes | no | no | (not applicable) | |
ref | yes | no | yes | yes |
out | yes | yes | yes | yes |
in | no | (not applicable) | (not applicable) | no |
An obvious question is why in is needed - afterall using default and just not assign would have the same result.
It is an optimization. Because modifying value is not allowed, then the compiler do not need to make a copy of the struct. So it is logical pass by value physical pass by reference.
I think it is a nice litle feature, but there is probably few cases where the performance improvement matters.
Similar to the above feature there is also an equivalent construct for return values.
By declaring something ref readonly.
Example:
using System;
namespace E
{
public struct S
{
public int A;
public int B;
public int C;
public S(int a, int b, int c)
{
A = a;
B = b;
C = c;
}
public override string ToString()
{
return string.Format("({0},{1},{2})", A, B, C);
}
}
public class Program
{
private static S s = new S(1, 2, 3);
public static ref readonly S M()
{
return ref s;
}
public static void Main(string[] args)
{
ref readonly S s = ref M();
// not allowed:
// s.A = 321;
Console.WriteLine(s);
}
}
}
Again I think it is a nice litle feature, but there is probably few cases where the performance improvement matters.
C# 7.2 also introduces readonly struct which let the compiler enforce a struct to be immutable.
Example:
using System;
namespace E
{
public readonly struct S
{
// non readonbly fields not allowed:
// public int x;
public int A { get; }
public int B { get; }
public int C { get; }
// setters now allowed:
// public int D { get; set; }
public S(int a, int b, int c)
{
A = a;
B = b;
C = c;
}
public override string ToString()
{
return string.Format("({0},{1},{2})", A, B, C);
}
}
public class Program
{
private static S s = new S(1, 2, 3);
public static S M()
{
return s;
}
public static void Main(string[] args)
{
S s = M();
Console.WriteLine(s);
}
}
}
I think this is a great feature. Guaranteed immutable value types are great for multi threaded code.
Span may seem like a small new feature, but it has potential to become a big new feature. There are lots of potential usage for it.
Fundamentally a Span is just a reference to a block of memory.
That sounds very simple, but there are several nice aspects:
Small example:
using System;
namespace E
{
public class Program
{
public static void Main(string[] args)
{
int[] ia = { 1, 2, 3, 4, 5 };
string s = "1 2 3 4 5";
// creating spans
Span<int> ias = ia;
Span<int> mias = ias.Slice(1, 3);
Console.WriteLine("{0} {1} {2}", mias[0], mias[1], mias[2]);
// using spans
mias.Reverse();
Console.WriteLine("{0} {1} {2} {3} {4}", ia[0], ia[1], ia[2], ia[3], ia[4]);
// creating readonly spans
ReadOnlySpan<char> ss = s.AsReadOnlySpan();
ReadOnlySpan<char> mss = ss.Slice(2, 5);
// using readonly spans
string s2 = new string(mss.ToArray());
Console.WriteLine(s2);
}
}
}
SOH (Small Object Heap) and LOH (Large Object Heap) now have separate locks to reduce lock contention.
(for those not aware LOH is used for objects over 85 KB)
.NET framework, .NET runtime, C# language/compiler and Visual Studio IDE each have their own version numbering scheme. For this release they are at 4.7.2, 4.0, 7.3 and 2017 update 7 respectively.
VS 2017 update 7 was released May 7th 2018.
This release is a rather small change.
It is now possible to add 3 new types of constraints on generic types:
The meaning of the first two are obvious. The meaning of the last one is that it is a type that can be moved to unmanaged space without marshalling/unmarshalling.
Example:
using System;
public class GenCon
{
public static void EnumEnum<T>(T o) where T : Enum
{
foreach(string nam in Enum.GetNames(typeof(T)))
{
Console.WriteLine("{0}{1}", nam, nam == o.ToString() ? "*" : "");
}
}
public enum A { X, Y, Z }
public class B { }
public static void Main(string[] args)
{
EnumEnum(A.Y);
}
}
It is now possible to compare value tuples directly.
C# 7.2 code:
using System;
public class TupComp72
{
public static void Main(string[] args)
{
var t1 = (a: 123, b: 456);
var t2 = (a: 123, b: 456);
Console.WriteLine(t1.a == t2.a && t1.b == t2.b);
}
}
C# 7.3 code:
using System;
public class TupComp73
{
public static void Main(string[] args)
{
var t1 = (a: 123, b: 456);
var t2 = (a: 123, b: 456);
Console.WriteLine(t1 == t2);
}
}
NET framework, .NET runtime, C# language/compiler and Visual Studio IDE each have their own version numbering scheme. For this release they are at 4.8, 4.0, 8.0 and 2019 respectively.
VS 2019 update 3 was released September 19th 2019.
C# 8.0 is a big change to the C# language with several important changes.
The old switch statement has been supplemented by a switch expression with a much shorter syntax in C# 8.0.
Syntax:
result = variable switch
{
value_1 => result_1,
...
value_n => result_n
}
C# 7.x code:
using System;
public class S7
{
public enum OTT { ONE, TWO, THREE }
public static void Test(OTT v)
{
string res;
switch(v)
{
case OTT.ONE:
res = "1";
break;
case OTT.TWO:
res = "2";
break;
case OTT.THREE:
res = "3";
break;
default:
res = "Ooops";
break;
}
Console.WriteLine(res);
}
public static void Main(string[] args)
{
Test(OTT.TWO);
Test((OTT)77);
}
}
C# 8.0 code:
using System;
public class S8
{
public enum OTT { ONE, TWO, THREE }
public static void Test(OTT v)
{
string res = v switch
{
OTT.ONE => "1",
OTT.TWO => "2",
OTT.THREE => "3",
_ => "Ooops"
};
Console.WriteLine(res);
}
public static void Main(string[] args)
{
Test(OTT.TWO);
Test((OTT)77);
}
}
This feature is good and well aligned with other modern languages. But I don't like the chosen syntax.
C# 8.0 also got tupple patterns.
C# 7.x code:
using System;
public class XS7
{
public enum Sign { POSITIVE, NEGATIVE }
public static Sign MultiplicationSign(Sign s1, Sign s2)
{
switch(s1)
{
case Sign.POSITIVE:
switch(s2)
{
case Sign.POSITIVE:
return Sign.POSITIVE;
case Sign.NEGATIVE:
return Sign.NEGATIVE;
}
break;
case Sign.NEGATIVE:
switch(s2)
{
case Sign.POSITIVE:
return Sign.NEGATIVE;
case Sign.NEGATIVE:
return Sign.POSITIVE;
}
break;
}
throw new Exception("We should never end here");
}
public static void Main(string[] args)
{
foreach(Sign s1 in Enum.GetValues(typeof(Sign)))
{
foreach(Sign s2 in Enum.GetValues(typeof(Sign)))
{
Console.WriteLine("{0} * {1} = {2}", s1, s2, MultiplicationSign(s1, s2));
}
}
}
}
C# 8.0 code:
using System;
public class XS8
{
public enum Sign { POSITIVE, NEGATIVE }
public static Sign MultiplicationSign(Sign s1, Sign s2)
{
return (s1, s2) switch
{
(Sign.POSITIVE, Sign.POSITIVE) => Sign.POSITIVE,
(Sign.POSITIVE, Sign.NEGATIVE) => Sign.NEGATIVE,
(Sign.NEGATIVE, Sign.POSITIVE) => Sign.NEGATIVE,
(Sign.NEGATIVE, Sign.NEGATIVE) => Sign.POSITIVE,
(_, _) => throw new Exception("We should never end here")
};
}
public static void Main(string[] args)
{
foreach(Sign s1 in Enum.GetValues(typeof(Sign)))
{
foreach(Sign s2 in Enum.GetValues(typeof(Sign)))
{
Console.WriteLine("{0} * {1} = {2}", s1, s2, MultiplicationSign(s1, s2));
}
}
}
}
I really like this feature and I suspect that other will too - it is simply an elegant way to express logic.
The using statement has been supplemented by a using declaration in C# 8.0.
Semantics is simply Dispose at end of declaration scope.
C# 7.x code:
using System;
namespace U7
{
public class Foobar : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed");
}
}
public class Program
{
public static void Main(string[] args)
{
using(Foobar fb = new Foobar())
{
Console.WriteLine("Doing something");
}
}
}
}
C# 8.0 code:
using System;
namespace U8
{
public class Foobar : IDisposable
{
public void Dispose()
{
Console.WriteLine("Disposed");
}
}
public class Program
{
public static void Main(string[] args)
{
using Foobar fb = new Foobar();
Console.WriteLine("Doing something");
}
}
}
Minor change in my opinion.
C# 7.0 introduced local non-static functions.
C# 8.0 introduces local static functions.
C# 7.x code:
using System;
public class LF7
{
public void Test()
{
void Test2()
{
Console.WriteLine("Local function");
}
Test2();
}
public static void Main(string[] args)
{
LF7 o = new LF7();
o.Test();
}
}
C# 8.0 code:
using System;
public class LSF8
{
public static void Test()
{
static void Test2()
{
Console.WriteLine("Local static function");
}
Test2();
}
public static void Main(string[] args)
{
Test();
}
}
Minor change in my opinion.
C# 8.0 allow to count array indexes from the end instead of from the beginning by prefixing index with ^.
Note that last element is ^1 and second last is ^2 and so on.
C# 8.0 allow to specify a range of an array as [start..end].
Note that start is inclusive and end is exclusive.
C# 7.x code:
using System;
public class R7
{
public static void Main(string[] args)
{
int[] a = { 101, 102, 103, 104, 105 };
Console.WriteLine(a[a.Length - 1]);
for(int i = 1; i < a.Length - 2; i++)
{
Console.WriteLine(a[i]);
}
}
}
C# 8.0 code:
using System;
public class R8
{
public static void Main(string[] args)
{
int[] a = { 101, 102, 103, 104, 105 };
Console.WriteLine(a[^1]);
foreach(int a1 in a[1..3])
{
Console.WriteLine(a1);
}
}
}
Minor change in my opinion.
C# 8.0 adds operator ??=:
a ??= b;
as the equivalent of:
a = (a != null) ? a : b;
C# 8.0 adds Deconstruct method to split up an object in pieces.
C# 7.x code:
using System;
public class Data
{
public int Iv { get; set; }
public String Sv { get; set; }
}
public class DC7
{
public static void Main(string[] args)
{
Data o = new Data { Iv = 123, Sv = "ABC" };
int iv = o.Iv;
string sv = o.Sv;
Console.WriteLine($"{iv} {sv}");
}
}
C# 8.0 code:
using System;
public class Data
{
public int Iv { get; set; }
public String Sv { get; set; }
public void Deconstruct(out int xiv, out string xsv)
{
xiv = Iv;
xsv = Sv;
}
}
public class DC8
{
public static void Main(string[] args)
{
Data o = new Data { Iv = 123, Sv = "ABC" };
(int iv, string sv) = o;
Console.WriteLine($"{iv} {sv}");
}
}
Nice little feature but not important in my opinion.
C# 8.0 has the option of changing reference types from by default to be nullable to by default to be non-nullable.
This makes C# similar to other modern languages like Kotlin.
The change is a setting and can be changed in different way. One way is command line compiler switch:
csc /nullable+ ...
C# 7.x code and C# 8.0 code with traditional setting:
using System;
public class N7
{
public class Foobar
{
}
public static void Main(string[] args)
{
string s = null;
Console.WriteLine(s == null);
Foobar o = null;
Console.WriteLine(o == null);
}
}
C# 8.0 code with new setting:
using System;
public class N8
{
public class Foobar
{
}
public static void Main(string[] args)
{
string? s = null;
Console.WriteLine(s == null);
Foobar? o = null;
Console.WriteLine(o == null);
}
}
This is a potential gigantic change to the way C# is written. Time will show whether the change catch on or not.
C# 8.0 added support for default methods in interfaces aka virtual extension methods.
They appear completely identical to interface default methods in Java 8+, but the underlying implementation is different and it impacts behavior. As a general rule always let implementing class explicit inherit from interface even if it inherits from a base class that inherits from the interface.
The intention is to make existing code using an interface working after the interface is being extended.
Example interface before extension:
public interface IMath
{
int Add(int a, int b);
int Sub(int a, int b);
}
Code using interface:
using System;
public class MathImpl : IMath
{
public int Add(int a, int b)
{
return a + b;
}
public int Sub(int a, int b)
{
return a - b;
}
}
public class Program
{
public static void Main(string[] args)
{
IMath m = new MathImpl();
Console.WriteLine(m.Add(123, 456));
}
}
C# 7.0 extended interface:
public interface IMath
{
int Add(int a, int b);
int Sub(int a, int b);
int Mul(int a, int b);
int Div(int a, int b);
}
The implementing code above does no longer compile, because its IMath implementation does not implement Mul and Div. It does not matter that the application does not use the two new methods.
C# 8.0 extended interface:
public interface IMath
{
int Add(int a, int b);
int Sub(int a, int b);
int Mul(int a, int b)
{
int res = 0;
for(int i = 0; i < b; i++) res += a;
return res;
}
int Div(int a, int b)
{
throw new Exception("Div not supported");
}
}
And now the implementing code above compiles fine, because IMath has default methods for Mul and Div. That Mul is slow and Div throw an exception does not matter since the application is not using those. If the application want to use those, then the implementation should add a good implementation of those methods.
Definitely useful. But not sure that people will be using it.
See .NET 5.0 and C# 9.0 New Features.
Version | Date | Description |
---|---|---|
1.0 | March 24th 2018 | Initial version |
1.1 | December 7th 2018 | Add C# 7.3 section |
1.1 | March 3rd 2019 | Add C# 8.0 section |
1.2 | April 12th 2019 | Update with C# 8.0 release |
1.3 | October 5th 2019 | More updates about C# 8.0 |
1.4 | November 12th 2020 | Some cleanup and add Deconstruct to C# 8.0 section |
1.5 | December 12th 2024 | Add interface default methods to C# 8.0 section |
See list of all articles here
Please send comments to Arne Vajhøj
I think Span has potential to become a great feature in C#/.NET.
But I do not see it become widely used with .NET 4.7.1 and C# 7.2 for a couple of reasons:
But when these issues get fixed, then I think Span is going to become a success.
Not because of the performance improvements, but because of the cleaner API's and code it will enable.