7 chap04
Jason Zhu edited this page 2021-08-11 23:44:49 +10:00

Chapter 4. Advanced C#

  • Summary: Introducing advanced C# topics built based on Chap2&3.
  • Recommended Reading Order: Read 4.1, 4.2, 4.3, 4.4 sequentially, and others in any order

4.1 Delegates

Delegate

  • Def: an object/instance that knows how to call a method
  • Its definition only defines called method's return type and parameter type. Any method with corresponding return & paramter is compatible with it.
// Defining delegate type
delegate int Transformer (int x);

// Compatible methods
int Square (int x) { return x * x; }
int MinusOne (int x) { return x - 1; }

// Create a delegate object/instance by assigning a method to a delegate variable
Transformer t = Square;

// Inovike delegate
int result = t(3);
// Above is shorthand of 
int result = t.Invoke(3);

Console.WriteLine(result); // 9

4.1.1 Writing Plug-in Methods with Delegates

  • Benefit of delegate: A delegate object is assigned a method at runtime. Hence can be used to write plug-in method.
delegate int Transformer (int x);
void Transform (int[] values, Transformer t)
// Transformer method has a delegate parameter, can be used to specifying an plug-in delegate object
{
    for (int i = 0; i < value; i++)
    {
        values[i] = t(values[i]);
    }
}

int Square (int x) => x * x;
int Cube (int x) => x * x * x;

int[] values = { 1,2,3, };
Transform(values, Square);
Transform(value, Cube); // Switch transformation by changing plugin delegates

Note:

  • Transform method is higher-order function, as it's function that takes a function as an argument
  • A method that returns a delegate is also a higher-order function

4.1.2 Instance and Static Method Targets

A delegate's target method (i.e. method to be bound) can be a local, static, or instance method

e.g. Target method of delegate is static

Transformer t = Text.Square; // Target method is static
Console.WriteLine(t(10));

class Test { public static int Square (int x) => x * x; }

delegate int Transformer (int x);

e.g. Target method of delegate is instance

delegate int Transformer (int x);
class Test { public int Square (int x) => x * x; }

Test test = new Test();
Transformer t = test.Square; // Create a delegate object and assign method to it

Console.WriteLine(t(10));
  • As delegate object is assigned with a instance method, it hence also maintains a reference to both the method and the instance that instance method belongs (i.e. test in this case). Hence, extend the lifetime of the instance object
    • System.Delegate class's Target property will reveal it.
public delegate void ProgressReporter (int percentComplete);
class MyReporter
{
    public string Prefix = "";
    public void ReportProgress (int percentComplete) => Console.WriteLine ( Prefix + percentComplete );
}

MyReporter r = new MyReporter();
r.Prefix = "%Complete: ";

ProgressReporter p = r.ReportProgress;  // Define a delegate object
p(99);                                  // %Complete: 99
Console.WriteLine(p.Target == r);       // True
Console.WriteLine(p.Method);            // Void ReportProgress(Int32)

r.Prefix = "";  // Change class object's property
p(99);          // 99

4.1.3 Multicast Delegates

All delegates instances have multicast capability:

  • A delegate instance can reference a list (multiple) of target methods

Operators to combine/remove delegate instances:

  • + or +=: combine delegate instances; e.g. SomeDelegate d = SomeMethod1; d += SomeMethod2
  • - or -=: remove right delegate from the delegate list; d -= SomeMethod1; Invoking d now only invoike SomeMethod2 lefted in d delegate object
  • A delegate object can be null.
  • Calling -= on delegate variable (which removed the single matching target) is equivalent to assigning null to that delegate instance.
  • Delegates are immutable; so when +=/-=, actually we are creating a new delegate instance and then assigning it to the existing delegate object.

Return value of multicast delegate object with nonvoid return type:

  • Caller receives return value of the last method
  • Preceding methods are still called/invoked, but return values are discarde.
  • Most multicast delegates return void.

A little detail:

  • All delegate types derives from System.MulticastDelegate, which inherits from System.Delegate

Multicast delegate example

public delegate void ProgressReporter (int percentComplete); // Define a delegate that 

// Define two methods to be invoked for progress report (compatible with the delegate)
void WriteProgressToConsole(int percentComplete) => Console.WriteLine(percentComplete);
void WriteProgressToFile(int percentComplete) => System.IO.File.WriteAllText("progress.txt", percentComplete.ToString());

// Create a multicast delegate instance
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile;

public class Util
{
    public static void HardWork (ProgressReporter p)
    {
        for (in i = 0; i < 10; i++)
        {
            p(i*10); // Invoke delegate to report progress
            System.Threading.Thread.Sleep(100); // Hard Work simulated using sleep
        }
    }
}

Util.HardWork(p); // Use caller to invoke methods through delegate

4.1.5 Generic Delegate Types

A delegate type can contain generic type parameter

public delegate T Transfromer<T> (T arg);

e.g. write a generalized Transform utility method works on any type:

public class Util
{
    public static void Transform<T> (T[] values, Tranformer<T> t)
    // Use generic delegate Transformer
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = t(values[i]);
        }
    }
}

// Create a method to be invoked
int Square (int x) => x*x;

int[] values = {1,2,3};
Util.Transform(values, Square); // Hook in Square
foreach (int i in values)
{
    Console.Write(i + " "); // 1,4,9
}

4.1.6 The Func and Action Delegates

Defined in System namespace, Func and Action delegates are delegate types that are so general they can work for methods with any return type and any number (up to 16) of arguments.

Declaration of these delegates:

delegate TResult Func<out TResult> ();
delegate TResult Func<int T, out TResult> (T arg);
delegate TResult Func<in T1, in T1, out TResult> (T1 arg1, T2 arg2);
//... up to T16

delegate void Action ();
delegate void Action<in T> (T arg);
delegate void Action<in T1, in T2> (T1 arg1, T2 arg2);
//... up to T16

As these delegates are extremely general, we can transform previous Transformer method to have a general Func delegate that takes a single arugment of type T and returns a same-typed value:

public static void Transform<T> (T[] values, Func<T,T> transformer)
// We can hook 
{
    for (int i = 0; i < values.Length; i++)
    {
        values[i] = transformer (values[i])
    }
}

4.1.7 Delegates vs Interfaces

Interface can have same effect as delegates

e.g. of using interface instead of delegate

public class Util
{
    public static void TransformAll (int[] values, ITransformer t)
    {
        for (int i = 0; i < values.Length; i++)
        {
            values[i] = t.Transform(values[i]);
        }
    }
}

public interface ITransformer
{
    int Transform (int x);
}

class Squarer : ITransformer
{
    public int Transform (int x) => x*x;
}

class Cuber : ITransformer
{
    public int Transform (int x) => x * x * x;
}

int[] values = { 1,2,3 };
Util.TransformAll(values, new Squarer());
Util.TransformAll(values, new Cuber());
foreach (int i in values)
{
    Console.WriteLine(i);
}

Use delegate over interface when:

  1. Interface defines only a single method
  2. Multicast capability is needed (e.g. call all delegate at once)
  3. Subscriber (i.e. Classes that implement interface) needs to implement the interface multiple time

4.1.8 Delegate Compatibility

Type Compatibility

  • Delegate types of different delegates are incompatible with one another, even if their signatures are same
  • Delegate instance are considered equal if they have same method target
  • Multicast delegates are equal if they reference same methods in same order
delegate void D1();
delegate void D2();
void Method1() {}

// Delegate types of different delegates are incompatible with one anther
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error

// Delegate types of same delegate are compatible
D1 d1 = Method1;
D1 d2 = Method1;
Console.WriteLine(d1 == d2); // True

Parameter Compatibility

  • Contravariance: A delegate can have more specific paramter types than its method target
delegate void StringAction(string s);

// Target method has is more general than parameter in delegate
void ActOnObject (object o) => Console.WriteLine (o);
StringAction sa = new StringAction(ActOnObject);
sa("hello");

Return type compatibility

  • Convariance: A delegate's target method might return a more specific type than described by the delegate
string RetrieveString() => "hello";
// Delegate is defined with a more general return type
delegate object ObjectRetriever();

ObjectRetiever o = new ObjectRetriever(RetrieveString);
object result = o();

Generic delegate type parameter variance

If defining a generic delegate type, good practice includes:

  • Mark a type parameter used only on the return value as convaiant (out)
  • Mark any type parameters used only on parameters as contravariant (in)
delegate TResult Func<out TResult>();

// which allow
Func<string> x = ...;

Summary:

Delegate object:

  • is a proxy object of compatible methods which has same return and parameter as delegate are defined
  • Class/Method can use delegate to swap methods dynamically

4.2 Events

4.2.1 Standard Event Pattern

4.2.2 Event Accessors

4.2.3 Event Modifiers

4.3 Lambda Expressions

4.3.1 Explicityly Specifying Lambda Parameter Types

4.3.2 Capturing Outer Variables

4.3.3 Lambda Expressions vs. Local Methods

4.4 Anonymous Methods

4.5 try Statements and Exceptions

4.6 Enumeration and Iterators

4.6.1 Enumeration

Enumerator = a read-only, forward-only cursor over a sequence of values.

C# treat a type as enumerator if it do either of three things:

  1. Has public method (without parameter) named MoveNext and property called Current
  2. Implement System.Collections.Generic.IEnumerator<T>
  3. Implement System.Collections.IEnumerator

Enumerable object is logical representation of a sequence

  • foreach statement can iterate over enumerable object
  • Enumerable object is not cursor by itself.

C# treat a type as enumerable object if it does any of following:

  1. Has a public parameterless method GetEnumerator that returns an enumerator
  2. Implements System.Collections.Generic.IEnumerable or its generic version System.Collections.Generic.IEnumerable<T>
  3. (C# 9) Can bind to an extension method named GetEnumerator that returns an enumerator

Pattern of enumerator and enumerable classes

class Enumerator // Class that typically implements IEnumerator or IEnumerator<T>
{
    public IteratorVariableType Current { get {...} }
    public bool MoveNext() {...}
}

class Enumerable // Class that typically implements IEnumerable or IEnumerable<T>
{
    public Enumerator GetEnumerator() {...}
}

Iterator through enumeralbe object

e.g. using foreach

foreach(char c in "beer")
// If enumerator implemenets IDisposable, the foreach statement also act as using statement, hence disposing enumerator object
{
    Console.WriteLine(c);
}

e.g. low-level way of iterating through string

using(var enumerator = "beer".GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var element = enumerator.Current;
        Console.WriteLine(element);
    }
}

4.6.2 Collection Initializers

Instantiation and population of an enumerable object can be done in single step

e.g. compiler translation of instantiation and population of enumerable object

using System.Collections.Generic;
...
List<int> list = new List<int> {1,2,3};

// which is translated into
List<int> list = new List<int>();
list.Add(1);
list.Add(2);

The translation (by compiler) requires enumerable object implements the System.Collections.IEnumerable interface, which has Add method.

4.6.3 Iterators

Iterator = producer of enumerator

e.g. use iterator to return sequence of Fibonacci number

using System;
using System.Collections.Generic;

IEnumerable<int> Fibs (int fibCount)
{
    for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
    {
        yield return prevFib; // "Here's the next element you asked me to yield from this enumerator 
        int newFib = prevFib + curFib;
        prevFib = curFib;
        curFib = newFib;

    }
}

foreach (int fib in Fibs(6))
{
    Console.Write(fib + " ");
}

What happen to iterator method (e.g. Fibs above)

  • Compiler converts iterator methods into private classes that implement IEnumerable<T> and/or IEnumerator<T>.
  • Logic within iterator block is "inverted" and spliced into MoveNext method and Current property.

4.6.4 Iterator Semantics (语义)

An iterator is a method, property or indexer that contains one or more yield statements.

An iterator must return one of these interfaces:

  • Enumerable interfaces
    • System.Collections.IEnumerable
    • System.Collections.Generic.IEnumerable<T>
  • Enumerator interfaces
    • System.Collections.IEnumerator
    • System.Collections.Generic.IEnumerator<T>

Iterator has different semantic depending on whether it returns enumerable interface or enumerator interface. Defails in Chap07

e.g. Multiple yield statement as shown below

foreach (string s in Foo())
{
    Console.WriteLine(s);
}

IEnumerable<string> Foo()
{
    yield return "One";
    yield return "Two";
}

yield break

Iterator block has NO return statement. Instead it use yield break statement to break early, without returning more elements

e.g. break early

IEnumerable<string> Foo (bool breakEarly)
{
    yield return "One";
    yield return "Two";

    if (breakEarly)
    {
        yield break;
    }

    yield return "Three";
}

Iterators and try/catch/finally blocks

  • Compiler will translate iterator into classes with MoveNext, Current and Dispose members.

Illegal try-catch block:

  • translating exception-handling blocks (i.e. yield return statement in try block of try-catch clause) will increase complexity and is illegal
IEnumerable<string> Foo()
{
    try { yield return "One"; } // Illegal
    catch { ... }
}

Legal try-final block:

  • It's legal to yield within a try block that has one finally block.
  • finally block executed when consuming enumerator reach end of sequence or disposed
  • Don't abandon enumeration early, you can dispose it by using statement (TODO: refer to textbook)
IEnumerable<string> Foo()
{
    try { yield return "One"; }
    finally { ... }
}

4.6.5 Composing Sequences

Iterators are composable, which is utilized in LINQ

e.g. output even Fibonacci number

using System;
using System.Collections.Generic;

foreach(int fib in EvenNumbersOnly(Fibs(6)))
{
    Console.WriteLine(fib);
}

IEnumerable<int> Fib(int fibCount)
{
    for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
    {
        yield return prevFib; // iterator for composing
        int newFib = prevFib + curFib;
        prevFib = curFib;
        curFib = newFib;
    }
}

IEnumerable<int> EvenNumbersOnly (IEnumerable<int> sequence)
{
    foreach (int x in sequence)
    {
        if ((x % 2) == 0)
        {
            yield return x; // iterator for composing
        }
    }
}

Each element is not calculated until when requested by MoveNext() operator data request and data output over time

4.7 Nullable Value Types

4.8 Nullable Reference Types

4.9 Extension Methods

4.9.1 Extension Methods Chaining

4.9.2 Ambiguity and Resolution

4.10 Anonymous Types

4.11 Tuples

4.11.1 Naming Tuple Elements

4.12.1 ValueTuple.Create

4.12.2 Deconstructing Tuples

4.12.3 Equality Comparison

4.12.4 The System.Tuple classes

4.12 Records (C# 9)

4.13 Patterns

4.13.1 var Pattenr

4.14.2 Constant Pattern

4.14.3 Relational Patterns (C# 9)

4.14.4 Pattern Combinators (C# 9)

4.14.5 Tuple and Positioanl Patterns

4.14.6 Property Pattern

4.14 Attributes

4.14.1 Attribute Classes

4.14.2 Named and Positional Attribute Parameters

4.14.3 Applying Attributes to Assemblies and Backing Fields

4.14.4 Specifying Multiple Attributes

4.15 Caller Info Attributes

4.16 Dynamic Binding

4.16.1 Static Binding vs. Dynamic Binding

4.16.2 Custom Binding

4.16.3 Language Binding

4.16.4 RuntimeBinderException

4.16.5 Runtime Representation of Dynamic

4.16.6 Dynamic Conversions

4.16.7 Dynamic Calls Without Dynamic Receivers

4.16.8 Static Types in Dynamic Expressions

4.16.9 Uncallable Functions

4.17 Operator Overloading

4.18 Operator Overloading

4.19 Unsafe Code and Pointers

4.20 Preprocessor Directives

4.21 XML Documentation

4.21.1 Standard XML Documentation Tags

4.21.2 User-Defined Tags

4.21.3 Type or Member Cross-References