.NET 4.7.1/4.7.2/4.8 and C# 7.1/7.2/7.3/8.0 New Features

Content:

  1. Version numbers
  2. .NET 4.6.2 and C# 7.1
    1. Release
    2. General
    3. Tuple enhancements
    4. default literal
  3. .NET 4.7.1 and C# 7.2
    1. Release
    2. General
    3. private protected
    4. in argument
    5. ref readonly declaration
    6. readonly struct
    7. Span
    8. GC enhancements
  4. .NET 4.7.2 and C# 7.3
    1. Release
    2. General
    3. Generic constraints
    4. Tuple comparison
  5. .NET 4.8 and C# 8.0
    1. Release
    2. Genereal
    3. switch expression
    4. using declaration
    5. static local function
    6. Indexes and ranges
    7. Null coalesce assignment
    8. Deconstruct
    9. Non-nullable reference types

Version numbers:

.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 4.6.2 and C# 7.1

Release:

.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.

General:

This is a very small update.

Tuple enhancements:

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.

default literal:

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 4.7.1 and C# 7.2

Release:

.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.

General:

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.

private protected:

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.

in argument:

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.

ref readonly declaration

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.

readonly struct

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:

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);
        }
    }
}

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.

GC enhancements:

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 4.7.2 and C# 7.3:

Release:

.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.

General:

This release is a rather small change.

Generic constraints:

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);
    }
}

Tuple comparison:

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 4.8 and C# 8.0:

Release:

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.

Genereal:

C# 8.0 is a big change to the C# language with several important changes.

switch expression:

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.

using declaration:

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.

static local function:

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.

Indexes and ranges:

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.

Null coalesce assignment:

C# 8.0 adds operator ??=:

a ??= b;

as the equivalent of:

a = (a != null) ? a : b;

Deconstruct:

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.

Non-nullable reference types:

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.

Next:

See .NET 5.0 and C# 9.0 New Features.

Article history:

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

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj