Functional C# (2) Function Type and Delegate

Datetime:2016-08-22 22:10:17          Topic: C#           Share

In C#, functions are represented by methods. C# methods/functions have types like just C# objects have types. C# object type is represented by class, and C# method/function’s type are represented by delegate type.

Delegate type

A delegate type is defined like a method signature with the delegate keyword. It represents a function type with the specified parameter types and return type. For example:

internal delegate void FuncToVoid();

Function type

FuncToVoid represents the function type that accepts no parameters, and returns void. For example, the following methods all accept no parameters, and return void:

namespace System.Diagnostics
{
    public sealed class Trace
    {
        public static void Close();

        public static void Flush();

        public static void Indent();
    }
}

So all of them are of type FuncToVoid.

The following delegate type represents the function type that accepts 1 string parameter, and returns void:

internal delegate void FuncStringToVoid(string value);

The following methods are all of FuncStringToVoid type:

namespace System.Diagnostics
{
    public sealed class Trace
    {
        public static void TraceInformation(string message);

        public static void Write(string message);

        public static void WriteLine(string message);
    }
}

These methods’ parameter names are different from the delegate type definition. In .NET, parameter names are ignored when identifying function types, only parameter types, their order, and return type matter.

The following delegate type represents the function type that accepts no parameter, and returns int:

internal delegate int FuncToInt32();

The following methods are all of FuncToInt32 type:

namespace System.Runtime.InteropServices
{
    public static class Marshal
    {
        public static int GetExceptionCode();

        public static int GetHRForLastWin32Error();

        public static int GetLastWin32Error();
    }
}

And the following delegate type represents the function type that accepts a string parameter, then a int parameter, and returns int:

internal delegate int FuncStringInt32ToInt32(string value1, int value2);

It is the type of the following methods:

namespace System.Globalization
{
    public static class CharUnicodeInfo
    {
        public static int GetDecimalDigitValue(string s, int index);

        public static int GetDigitValue(string s, int index);
    }
}

Again, the parameter names are ignored.

Generic delegate type

Above FuncToInt32 represents the function type that accepts no parameters and return int. Later if function types are needed to represents similar function types, like accepting no parameters, and returning bool, string, or object, then the following delegate types can be defined:

internal delegate bool FuncToBoolean();

internal delegate string FuncToString();

internal delegate object FuncToObject();

The definitions can continue forever for different returning types. Fortunately, C# introduced generic programming since 2.0. Generic programming is a paradigm that supports type parameters, so that type information are allowed to be provided later. In the above series of delegate type defections, the return type changes, so replace it with a type parameter with any name, like TResult:

internal delegate TResult Func<TResult>();

Here TResult is type parameter, and used as the return type. It is just a placeholder to be specified with concrete type later. When TResult is int, Func<int> represents the function type that accepts no arguments and return int, which is equivalent to FuncReturnInt32, and Func<bool> is equivalent to FuncToBoolean, and Func<string> is equivalent to FuncToString, Func<object> is equivalent to FuncToObject, etc. All the delegate types in this pattern can be replaced by the single generic delegate type Func<TResult>.

Since Func<int> and FuncToInt32 are equivalent, The above Marshal.GetExceptionCode, Marshal.HRForLastWin32Error, Marsha.GetLastWin32Error methods are of Func<int> type too.

In .NET, a generic type with type parameters are called open type. If generic type’s all type parameters are specified with concrete types, then it is called closed type. Here Func<TResult> is open type, and Func<int>, Func<string>, Func<bool> are closed types.

Here is another example:

internal delegate TResult Func<T1, T2, TResult>(T1 value1, T2 value2);

So Func<string, int, int> is equivalent to above FuncStringInt32ToInt32, the CharUnicodeInfo.GetDecimalDigitValue and CharUnicodeInfo.GetDigitalValue methods are of Func<string, int, int> too. The following 4 methods:

namespace System
{
    public static class Math
    {
        public static double Log(double a, double newBase);

        public static int Max(int val1, int val2);

        public static double Round(double value, int digits);

        public static decimal Round(decimal d, MidpointRounding mode);
    }
}

are of Func<double, double, double>, Func<int, int, int>, Func<double, int, double> and Func<decimal, MidPointRounding, decimal> types.

Unified built-in delegate types

As fore mentioned, delegate types can be defined with duplicate, like Func<int> and FuncToInt32, Func<string, int, int> and FuncStringInt32ToInt32, etc. The following delegate types are defined in mscorlib.dll since .NET 2.0:

namespace System
{
    public delegate int Comparison<in T>(T x, T y);
}

The following custom delegate types can be defined too:

internal delegate int NewComparison<in T>(T x, T y);

internal delegate TResult FuncStringString<TResult>(string value1, string value2);

internal delegate int FuncToInt32<T1, T2>(T1 value1, T2 value2);

internal delegate int FuncStringStringToInt32(string value1, string value2);

As a result, Func<string, string, int>, Comparison<string>, NewComparison<int>, FuncStringString<int>, FuncToInt32<string, string>, FuncStringStringToInt32 all represent function type that accepts 2 string parameters, and returns int. They are all equivalent function types.

Even built-in delegate types duplicate. The following delegate types are defined in mscorlib.dll since .NET 2.0, and they all represent one function type:

namespace System.Threading
{
    public delegate void SendOrPostCallback(object state);

    public delegate void ContextCallback(object state);

    public delegate void ParameterizedThreadStart(object obj);

    public delegate void WaitCallback(object state);

    public delegate void TimerCallback(object state);
}

To avoid all kinds of duplication, since .NET 3.5, FCL provides 2 series of built-in delegate types to unify all the function types. The following Action delegate types can represent all function types with 0 ~ 16 parameters, and returning void:

namespace System
{
    public delegate void Action();

    public delegate void Action<in T>(T obj);

    public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);

    public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);

    public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

    // ...

    public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);
}

The in/out keyword before the type parameter specifies that type parameter is contravariant/covariant, which will be discussed in detail later.

The following Func delegate types can represent all function types with 0 ~ 16 parameters, and not returning void:

namespace System
{
    public delegate TResult Func<out TResult>();

    public delegate TResult Func<in T, out TResult>(T arg);

    public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);

    public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);

    public delegate TResult Func<in T1, in T2, in T3, in T4, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);

    // ...

    public delegate TResult Func<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12, T13 arg13, T14 arg14, T15 arg15, T16 arg16);
}

In the latest .NET FCL, the Action/Func delegate types with 0 ~ 8 parameters are defined in mscorlib.dll, and the Action/Func delegate types with 9 ~ 8 parameters are defined in System.Core.dll.

Regarding consistency, This tutorial uses these delegate types for all function types. For example:

namespace System.Globalization
{
    public class SortKey
    {
        public static int Compare(SortKey sortkey1, SortKey sortkey2);
    }
}

The above method’s type should be only represented with System.Func<SortKey, SortKey, int>, not System.Comparison<SortKey> or any other delegate type.

Delegate instance

Just like object can be instantiated from class, delegate instance can be instantiated from delegate type.

Function

Delegate instance represents a function. The instantiation syntax is similar to the constructor call when instantiating an object:

internal static partial class Functions
{
    internal static void Constructor()
    {
        Func<int, int, int> func = new Func<int, int, int>(Math.Max);
        int result = func(1, 2);
        Trace.WriteLine(result); // 2
    }
}

The constructor call syntax can be omitted:

internal static void Instantiate()
{
    Func<int, int, int> func = Math.Max;
    int result = func(1, 2);
    Trace.WriteLine(result); // 2
}

With this syntax, above paradigm looks very functional. Func<int, int, int> is the function type, maxFunction variable is the function instance, and it is initialized with the Math.Max method. And naturally, function maxFunction can be called. When it is called, Math.Max executes and return the result.

Delegate class and delegate object

The above functional paradigm is actually implemented by wrapping imperative object-oriented programming. For each delegate type definition, C# compiler actually generate a class definition. Here, System.Func<T1, T2, TResult> delegate type is compiled to the following class:

public sealed class CompiledFunc<in T1, in T2, out TResult> : MulticastDelegate
{
    public CompiledFunc(object @object, IntPtr method)
    {
    }

    public virtual TResult Invoke(T1 arg1, T2 arg2)
    {
    }

    public virtual IAsyncResult BeginInvoke(T1 arg1, T2 arg2, AsyncCallback callback, object @object)
    {
    }

    public virtual void EndInvoke(IAsyncResult result)
    {
    }
}

The class has a Invoke method, with the same signature as the delegate type. So above delegate instantiation code is compiled to normal object instantiation, and the function call is compiled to above Invoke method call:

internal static void CompiledInstantiate()
{
    CompiledFunc<int, int, int> func = new CompiledFunc<int, int, int>(null, Math.Max);
    int result = func.Invoke(1, 2);
    Trace.WriteLine(result); // 2
}

All delegate types are derived from System.MulticastDelegate, and MulticastDelegate is derived from System.Delegate:

namespace System
{
    [ClassInterface(ClassInterfaceType.AutoDual)]
    [ComVisible(true)]
    public abstract class Delegate : ICloneable, ISerializable
    {
        public MethodInfo Method { get; }

        public object Target { get; }

        public static bool operator ==(Delegate d1, Delegate d2);

        public static bool operator !=(Delegate d1, Delegate d2);

        // Other members.
    }
}

So each delegate instance inherits the Method and Target properties too. The following example demonstrates the members of delegate instance:

internal static void Static()
{
    Func<int, int, int> func1 = Math.Max;
    int result1 = func1.Invoke(1, 2);
    MethodInfo method1 = func1.Method;
    Trace.WriteLine($"{method1.DeclaringType}: {method1}"); // System.Math: Int32 Max(Int32, Int32)
    Trace.WriteLine(func1.Target == null); // True

    Func<int, int, int> func2 = Math.Max;
    Trace.WriteLine(func1 == func2);
}

As fore mentioned, func1 look like a function, but it is essentially an object. It has a Invoke method accepting 2 int parameters and return int. Its Method property gets the underlying method, and its Target property gets the underlying method’s host object. Here func1’a underlying method is a static method, so its Target property returns null. When 2 delegate instance’s underlying static methods are the same, then == operator returns true.

In contrast, the following delegate instance’s underlying method is an instance method:

internal static void Instance()
{
    WebClient webClient1 = new WebClient();
    Func<string, string> func1 = webClient1.DownloadString;
    string result2 = func1.Invoke("https://weblogs.asp.net/dixin"); // string result2 = func1("https://weblogs.asp.net/dixin");
    MethodInfo method2 = func1.Method;
    Trace.WriteLine($"{method2.DeclaringType}: {method2}"); // System.Net.WebClient: System.String DownloadString(System.String)
    Trace.WriteLine(object.ReferenceEquals(func1.Target, webClient1)); // True

    WebClient webClient2 = new WebClient();
    Func<string, string> func2 = webClient2.DownloadString;
    Trace.WriteLine(func1 == func2); // False

    Func<string, string> func3 = webClient1.DownloadString;
    Trace.WriteLine(func1 == func3); // True
}

So the Target property returns the webClient1 object, where DownloadString is a method member. When When 2 delegate instance’s underlying instance methods are the same, and the host objects are also the same, then == operator returns true.

Function group

Besides function, delegate instance can also represent function groups. The following methods are all of Func<string> type:

internal static string A()
{
    Trace.WriteLine(nameof(A));
    return nameof(A);
}

internal static string B()
{
    Trace.WriteLine(nameof(B));
    return nameof(B);
}

internal static string C()
{
    Trace.WriteLine(nameof(C));
    return nameof(C);
}

internal static string D()
{
    Trace.WriteLine(nameof(D));
    return nameof(D);
}

They can be combined/uncombined with the +/- operators:

internal static void Group()
{
    Func<string> a = A;
    Func<string> b = B;
    Func<string> functionGroup1 = a + b;
    functionGroup1 += C;
    functionGroup1 += D;
    string lastResult1 = functionGroup1(); // A B C D
    Trace.WriteLine(lastResult1); // D

    Func<string> functionGroup2 = functionGroup1 - a;
    functionGroup2 -= D;
    string lastResult2 = functionGroup2(); // B C
    Trace.WriteLine(lastResult2); // C

    Func<string> functionGroup3 = functionGroup1 - functionGroup2 + a;
    string lastResult3 = functionGroup3(); // A D A
    Trace.WriteLine(lastResult3); // 8
}

Here functionGroup1 is combination of (A + B + C + D). When functionGroup1 is called, the 4 internal functions are called one by one, so functionGroup1’s return value is the last function D’s return value. functionGroup2 is (functionGroup1 – A – D), which is (B + C), so functionGroup2’s return value is C. functionGroup3 is (functionGroup1 – functionGroup2 + A), which is (A + B + A), so its return value is A. Actually, + is compiled to Delegate.Combine call and – is compiled to Delegate.Remove call:

internal static void CompiledGroup()
{
    Func<string> a = A;
    Func<string> b = B;
    Func<string> functionGroup1 = (Func<string>)Delegate.Combine(a, b); // = a + b;
    functionGroup1 = (Func<string>)Delegate.Combine(functionGroup1, new Func<string>(C)); // += C;
    functionGroup1 = (Func<string>)Delegate.Combine(functionGroup1, new Func<string>(D)); // += D;
    string lastResult1 = functionGroup1.Invoke(); // A B C D
    Trace.WriteLine(lastResult1); // D

    Func<string> functionGroup2 = (Func<string>)Delegate.Remove(functionGroup1, a); // = functionGroup1 - a;
    functionGroup2 = (Func<string>)Delegate.Remove(functionGroup2, new Func<string>(D)); //  -= D;
    string lastResult2 = functionGroup2.Invoke(); // B C
    Trace.WriteLine(lastResult2); // C

    Func<string> functionGroup3 = (Func<string>)Delegate.Combine( // = functionGroup1 - functionGroup2 + a;
        (Func<string>)Delegate.Remove(functionGroup1, functionGroup2), a);
    string lastResult3 = functionGroup3(); // A D A
    Trace.WriteLine(lastResult3); // 8
}

To keep it simple and consistent, this tutorial use delegate instance to represent function, and does not use the function group features.





About List