Table of Contents
- Chapter 4. Advanced C#
- 4.1 Delegates
- 4.1.1 Writing Plug-in Methods with Delegates
- 4.1.2 Instance and Static Method Targets
- 4.1.3 Multicast Delegates
- 4.1.5 Generic Delegate Types
- 4.1.6 The Func and Action Delegates
- 4.1.7 Delegates vs Interfaces
- 4.1.8 Delegate Compatibility
- Type Compatibility
- Parameter Compatibility
- Return type compatibility
- Generic delegate type parameter variance
- Summary:
- 4.2 Events
- 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
- 4.6.2 Collection Initializers
- 4.6.3 Iterators
- 4.6.4 Iterator Semantics (语义)
- 4.6.5 Composing Sequences
- 4.7 Nullable Value Types
- 4.8 Nullable Reference Types
- 4.9 Extension Methods
- 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
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 objectSystem.Delegate
class'sTarget
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;
Invokingd
now only invoikeSomeMethod2
lefted ind
delegate object- A delegate object can be
null
. - Calling
-=
on delegate variable (which removed the single matching target) is equivalent to assigningnull
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 fromSystem.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:
- Interface defines only a single method
- Multicast capability is needed (e.g. call all delegate at once)
- 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:
- Has public method (without parameter) named
MoveNext
and property calledCurrent
- Implement
System.Collections.Generic.IEnumerator<T>
- 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:
- Has a public parameterless method
GetEnumerator
that returns an enumerator - Implements
System.Collections.Generic.IEnumerable
or its generic versionSystem.Collections.Generic.IEnumerable<T>
- (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/orIEnumerator<T>
. - Logic within iterator block is "inverted" and spliced into
MoveNext
method andCurrent
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
andDispose
members.
Illegal try-catch block:
- translating exception-handling blocks (i.e.
yield return
statement intry
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 atry
block that has onefinally
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