Table of Contents
- Chapter 3. Creating Types in C#
- 3.1 Classes
- 3.1.1 Fields
- 3.1.2 Constants
- 3.1.3 Instance Constructors
- 3.1.4 Deconstructors
- 3.1.5 Object Initializers
- 3.1.6 The this Reference
- 3.1.7 Properties
- Read-only and calculated properties
- Expression-bodied properties
- Automatic properties (C# 3.0)
- Property initializer
- get and set accessibility
- init-only setters (C# 9)
- CLR property implementation
- Summary
- 3.1.8 Indexers
- 3.1.9 Static Constructors
- 3.1.10 Static Classes
- 3.1.11 Finalizers
- 3.1.12 Partial Types and Methods
- 3.1.13 The nameof operator
- 3.2 Inheritance
- 3.2.1 Polymorphism
- 3.2.2 Casting and Reference Conversions
- 3.2.3 Virtual Function Members
- 3.2.4 Abstract Classes and Abstract Members
- 3.2.5 Hiding Inherited Members
- 3.2.6 Sealing Functions and Classes
- 3.2.7 The base Keyword
- 3.2.8 Constructors and Inheritance
- 3.2.9 Overloading and Resolution
- 3.3 The object Type
- 3.3.1 Boxing and Unboxing
- 3.3.2 Static and Runtime Type Checking
- 3.3.3 The GetType Method and typeof Operator
- 3.3.4 The ToString Method
- 3.3.5 Object Member Listing
- 3.4 Structs
- 3.5 Access Modifiers
- 3.6 Interfaces
- 3.6.1 Extending an Interface
- 3.6.2 Explicit Interface Implementation
- 3.6.3 Implementing Interface Member Virtually
- 3.6.4 Reimplementing an Interface in a Subclass
- 3.6.5 Interfaces and Boxing
- 3.6.6 Default Interface Members
- 3.7 Enums
- 3.8 Nested Types
- 3.9 Generics
- 3.9.1 Generic Types
- 3.9.2 Why Generic Exist
- 3.9.3 Generic Methods
- 3.9.4 Declaring Type Parameters
- 3.9.5 typeof and Unbound Generic Types
- 3.9.6 The default Generic Value
- 3.9.7 Generic Constraints
- 3.9.8 Subclassing Generic Types
- 3.9.9 Self-Referencing Generic Declarations
- 3.9.10 Static Data
- 3.9.11 Type Parameters and Conversions
- 3.9.12 Covariance
- 3.9.13 Contravariance
- 3.9.14 C# Generics vs. C++ Templates
Chapter 3. Creating Types in C#
3.1 Classes
3.1.1 Fields
3.1.2 Constants
3.1.3 Instance Constructors
3.1.4 Deconstructors
3.1.5 Object Initializers
3.1.6 The this
Reference
this
reference refers to the instance itselfthis
reference disambiguate a local variable or parameter from a field (of the instance)
public class Test
{
string name;
public Test (string name)
{
this.name = name;
}
}
3.1.7 Properties
Properties look like fields from outside, but inside contain logic for geeter and setter
e.g. Property from outside
Stock msft = new Stock();
msft.CurrentPrice = 30; // looks like fileds
msft.CurrentPrice -= 3; // Contain logic
Declaration of Properties: similar as fields but with get/set
blocks:
get
&set
are property accessorsset
accessor has implicit parameter namedvalue
of the property type. So we can usevalue
to assign to a private field- Typically has a dedicated backing field (i.e. private field)
- Benefit of accessor:
- Provide control that enable the implementer to choose internal representation without exposing internal details to user of property
e.g. CurrentPrice
property
public class Stock
{
// Field of the class
decimal currentPrice; // private "backing" field
// Property of the class
public decimal CurrentPrice; // public property
{
get { return currentPrice; }
set { currentPrice = value; } // can throw an exception if `value` was outside a valid range of values
}
}
Read-only and calculated properties
- Read-only property: property with only a
get
accessor- can be computed from other data
- Write-only property (Rarely Used): with only a
set
accessor
public class Stock
{
decimal currentPrice, sharesOwned;
public decimal Worth
{
get { return currentPrice * sharesOwned; }
}
}
Expression-bodied properties
Expression-bodied properties: shortened format of properties declaration.
- Use a fat arrow to replace all
{}
andget/set/return
keywords
e.g. expression-bodied property
public decimal Worth
{
get => currentPrice * sharesOwned; // get accessor of expression-bodied property
set => sharesOwned = value / currentPrice; // set accessor
}
Automatic properties (C# 3.0)
As most common implementation for a property is simple getter & setter, Automatic property declaration instructs compiler to generate a private backing field that cannot be referred to:
private
orprotected
can be added onset
for exposing the property as read-only to other types
public class Stock
{
...
public decimal CurrentPrice { get; set; } // Automatic property
}
Property initializer
Property initializer can be added to automatic property like fileds:
public decimal CurrentPrice { get; set; } = 123; // gives CurrentPrice an initial value of 123
e.g. read-only property with initializer
public int Maximum { get; } = 999;
get and set accessibility
get/set
can have different access levels
- common usage: have a
public
property with aninternal/private
setter
public class Foo
{
private decimal x;
public decimal X
{
get { return x; }
private set {x = Math.Round(value,2); } // setter is less accessible
}
}
init-only setters (C# 9)
Declare a property accessor init
instead of set
:
- The propety is read-only
- It can be set via object initializer (e.g. below). After that, this property cannot be altered.
- Init-only properties cannot even be set from inside their class, except initalizer, constructor
public class Note
{
public int Pitch { get; init; } = 20; // "Init-only" property
public int Duration { get; init } = 100;
}
var note = new Note { Pitch = 50 };
note.Pitch = 200; // Error init-only setter!
TODO: Not finished for init-only property
CLR property implementation
TODO: Omitted, may come back in future
Summary
Property is like field but with internal logic for getting/setting the backing field
public class RandomClass
{
public Property1 { get; set; }
public Property2_ReadOnly { get; }
}
3.1.8 Indexers
Indexer provide a natural syntax for accessing elements in a class or struct that has a list or dictionary of values
- Access indexed value: via index argument (similar as use property name to access property). Index argument can be any type
e.g. string
class use index to access char
values
string s = "hello";
Console.WriteLine(s[0]); // 'h'
Console.WriteLine(s[3]); // 'l'
Implementing and indexer
Steps:
- Define a property called
this
(specified in[]
) - Write
get/set
in the propertythis
Advaned Indexer:
- A class/struct can have multiple indexer, each with parameter of different types
- Read-only indexer: A indexer with
get
accessor only
e.g. define & use indexer
// Define indexer
class Sentence
{
string[] words = "The quick brown forx".Split();
public string this [int wordNum] // indexer
{
get { return words[wordNum]; }
set { words[wordNum] = value; }
}
// Second indexer using multiple parameter
public string this [int arg1, string arg2]
{
get { ... }
set { ... }
}
// Read-only indexer (expression-bodied syntax version)
public string this [int wordNum] = word[wordNum];
}
// Use indexer
Sentence s = new Sentence();
Console.WriteLine(s[3]); // fox
s[3] = "Kangaro";
Console.WriteLIne(s[3]); // Kangaroo
CLR indexer implementation
TODO: omitted
Using indices and ranges with indexers
Indexer can be created to support indices and rangers using Index
or Range
e.g. Sentence class with indexers
// Define a class with indices and range
class Sentence
{
public string this [Index index] => words[index];
public string[] this [Range range] => words[range];
}
// use indices and range indexers
Sentence s = new Sentence();
Console.WriteLine(s[^1]); // fox
string[] firstTwoWords = s[..2]; // (The, quick)
3.1.9 Static Constructors
Define & Declare static constructor:
- Constructor get executed once per type
- Each type define only 1 static constructor
- static constructor is parameterless and have the same name as type, as shown below
e.g. declare a static constructor
class Test
{
// static constructor of the class
static Test() { Console.WriteLine("Type Initialized"); }
}
When runtime will invoke static constructor invoked. 2 triggers:
- Instantiating the type
- Accessing a static member
Only 2 modifiers allowed: unsafe
and extern
Static constructors and field initialization order
e.g. static field intiailizer
class Foo
{
// Static field initializer run in order
public static int X = Y; // 0
public static int Y = 3; // 3
}
When static field/constructor initializer run:
- Static field initializer run just before static constructor called.
- If there is no static constructor, static field initializer executed before type being used in runtime
3.1.10 Static Classes
Static class means it only consists of static members and cannot be subclassed.
e.g. System.Console
, System.Math
3.1.11 Finalizers
Finalizer = class-only methods executed before the garbage collector reclaims memory for unreferenced objects.
class Class1
{
~Class1() // C# syntax for overriding Object's Finilize method
{
...
}
}
Details of Finalizer is in Chap12
3.1.12 Partial Types and Methods
Partial Types (e.g. partial class, partial struct) allow definition to be split into multiple files.
- e.g. a partial class is autogenerated using template; that class is enhanced with hand-authored content in another file
Syntax Required:
- Each participant (i.e. partial types) require
partial
declaration - All participants cannot have conflicting member (i.e. same paramter, same return)
- Partial types are resolved at compile time.
e.g. partial type in different files
// auto-generated file - PaymentFormGen.cs
partial class PaymentForm {...}
// hand-authored - PaymentForm.cs
partial class PaymentForm {...}
Partial methods
A partial type can contain partial methods. So autogenerated partial type can provide customizable hooks for manual change
Syntax of partial methods:
- consists of 2 part: a definition (in auto-generated file) and an implementation (in hand-authored file)
- If implementation is not provided, definition of the partial method is compiled anyway.
- Partial methods is
void
, and implicitlyprivate
, cannot includeout
parameter
// In auto-generated file e.g. PartialFileGen.cs
partial class PaymentForm
{
...
partial void ValidatePayment(decimal amount); // Definition
}
// In hand-authored file
partial class PaymentForm
{
...
partial void ValidatePayment(decimal amount) // implementation
{
if (amount > 100)
...
}
}
Extended partial methods (C# 9)
TODO: Omit
3.1.13 The nameof
operator
nameof
operator returns name of any symbol (type, memeber, variable, etc.) as a string- Advantage of
nameof
: static type checking, e.g. if rename the symbol, all references will be renamed
int count = 123;
string name = nameof(count); // name is "count"
3.2 Inheritance
Subclass inherit from Superclass
e.g. define class inheritance
// Superclass
public class Asset
{
public string Name;
}
// Subclass
public class Stock : Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortgage;
}
e.g. use classes
Stock msft = new Stock { Name = "MSFT",
SharesOwned = 1000 };
Console.WriteLine(msft.Name); // MSFT
Console.WriteLine(msft.SharesOwned); // 1000
House mansion = new House { Name = "Mansion",
Mortgage = 2500000 };
3.2.1 Polymorphism
Polymorphism (多态): References (e.g. class) are polymorphic (i.e. a variable of type x
can refer to an object that subclasses x
)
- As all subclasses have field/properties/methods of superclass, so it's safe
// Define a method that try to access superclass
public static void Display (Asset asset)
{
System.Console.WriteLine(asset.Name)
}
// As Stock is subclass of Asset, Display can access it
Stock msft = new Stock ...;
House mansion = new House ...;
Display(msft); // msft is subclass of asset
Display(mansion); // mansion is subclass of asset
3.2.2 Casting and Reference Conversions
An object reference can be:
- Implicitly upcast to a base class (superclass) reference
- Explicitly downcast to a subclass reference
TODO: Omit remaining content
Upcasting
Downcasting
The as operator
The is operator
Introducing a pattern variable
3.2.3 Virtual Function Members
- Virtual Functions: A function marked as
virtual
can be overriden by subclasses for more specific implementation (Note: it also has its own implementation) - Who be declared
virtual
? ANS: Methods, properties, indexers, events
// Define a virtual method to be overridden
public class Asset
{
public string Name;
public virtual decimal Liability => 0; // Expression-bodied properties
}
// subclass override
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage;
}
// Initialization of overridden field
House mansion = new House { Name = "McMansion", Mortgage = 250000 };
Asset a = mansion;
Console.WriteLine(a.Liability); // 250000
Covariant return types (C# 9)
TODO: Omit
3.2.4 Abstract Classes and Abstract Members
- Abstract class: a class declared as abstract can never be instantiated. Only its concrete subclasses can be instantiated.
- Abstract classes have abstract members. They are like virtual members (e.g. functions, properties), but without a default implementation.
- Implementation is provided by its concrete subclasses, unless subclasses are also abstract
public abstract class Asset
{
// Note empty implementation
public abstract decimal NetValue { get; }
}
public class Stock : Asset
{
public long SharesOwned;
public decimal CurrentPrice;
// Override abstract
public override decimal NetValue => CurrentPrice * SharesOwned;
}
3.2.5 Hiding Inherited Members
- When base class and subclass are defined with identical members, member in subclass hide the member in superclass
- When encountered, compiler generates a warning and then resolves the ambiguity as follow:
- References to method
Counter
in superclassA
(at compile time) bind toA.Counter
- References to method
Counter
in subclassB
(at compile time) bind toB.Counter
Counter
inB
is said to hide theCounter filed
in classA
- References to method
- Solution for deliberate hiding, apply
new
modifier to member in subclass. It only suppress compiler warning, not fix problem
public class A { public int Counter = 1; }
public class B : A { public new int Counter = 2; }
new
vs override
new
only hide base class's method. You can still refer to usingA.Method
override
will change base class's method
public class BaseClass
{
public virtual void Foo()
// Will be overriden or hidden
{
Console.WriteLine("BaseClass.Foo")
}
}
public class Overrider : BaseClass
{
public override void Foo() {
Console.WriteLine("Overrider.Foo");
}
}
public class Hider : BaseClass
{
public new void Foo()
{
Console.WriteLine("Hider.Foo");
}
}
// Show Difference
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // Overrider.Foo
b1.Foo(); // Overrider.Foo, changed base's implementation
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); // Hider.Foo
b2.Foo(); // BaseClass.Foo
3.2.6 Sealing Functions and Classes
sealed
keyword: used to seal implementation of an overriden function method to prevent it from being overriden in further subclasses.
- Both functions and classes can be
sealed
- Sealing a class is more common
- We cannot seal a hidden member
public class Asset
{
public string Name;
public virtual decimal Liability => 0;
}
public class House : Asset
{
public decimal Mortgage;
public sealed override decimal Liability => Mortgage;
}
3.2.7 The base Keyword
3.2.8 Constructors and Inheritance
3.2.9 Overloading and Resolution
3.3 The object Type
object
(System.object
) is the base class for all types- Any type can be upcast to
object
e.g. designing a general-purpose stack
(栈, a LIFO data structure) can store 10 objects
public class Stack
{
int position;
object[] data = new object[10];
// We can push or pop any type to and from Stack
public void Push (object obj)
{
data[position++] = obj;
}
public object Pop()
{
return data[--position];
}
}
object
is a reference type, as result of being a class.- type unification: Any subclass can be cast from and to
object
. e.g. below
stack.Push(3);
int three = (int) stack.Pop(); // cast from object using type unification
3.3.1 Boxing and Unboxing
Boxing & Unboxing:
- Boxing = act of converting a value-type instance to a reference-type instance. Reference type can be
object
class or interface - Unboxing = (reverse operation) casting object instance back to original value type
- Unboxing requires explicit cast: CLR check whether proposed value type match actual object type, and throw
InvalidCastException
if check fails.
- Unboxing requires explicit cast: CLR check whether proposed value type match actual object type, and throw
int x = 9
object obj = x; // Box the int
int y = (int) obj; // Unbox the int
e.g. unboxing checking
object obj = 9;
long x = (long) obj; // InvalidCastException
object obj = 3.5;
int x = (int) (double) obj; // (double) perform unboxing, (int) perfrom conversion
Copying semantic of boxing and unboxing
- Boxing copies value-type instance into the new object (
object
or instance) - Unboxing copies content of object into a value type instance.
- So, there is no reference copy, hence changing value after Boxing/Unboxing won't affect boxed/unboxed copy
int i = 3;
object boxed = i; // instance copied into new object
i = 5;
Console.WriteLine(boxed); // 3, as instanced already copied, no change
3.3.2 Static and Runtime Type Checking
C# program will be performed 2 kings of type checking:
- Static type checking: enable compiler to verify correctness of program without running; (e.g.
int x = "5"
) - Runtime type checking: performed by CLR when downcast via a reference conversion or unboxing
- It's possible as each object on heap has a little type token, which can be retrieved by calling
GetTyep
method ofobject
- It's possible as each object on heap has a little type token, which can be retrieved by calling
e.g. Runtime type checking
object y = "5";
int z = (int) y; // Runtime error, downcast failed
3.3.3 The GetType
Method and typeof
Operator
All C# types/objects at runtime has a instance of System.Type
. 2 ways to get:
- Call
.GetType()
on instance: evaluated at runtime - Use
typeof()
on a type name: evaluated at compile time. hence, type information of a type is static
System.Type
instance has many properties; e.g. name, assembly, base type, etc.
e.g. properties of System.Type
instances
Point p = new Point();
Console.WriteLine(p.GetType().Name); // Point
Console.WriteLine(p.GetType() == typeof(Point));
Console.WriteLine(typeof(Point).Name); // Point
...
3.3.4 The ToString
Method
Every C# object has ToString
method to return default textual representation of type instance.
- For all built-in types,
ToString
method is overridden accordingly - We should also override
ToString
method for our custom types
e.g. ToString method on built-in type
int x = 1;
string s = x.ToString(); // s is "1"
e.g. override ToString method of custome type
Panda p = new Panda { Name = "Petry" };
Console.WriteLine(p); // Petey
public class Panda
{
public string Name;
public override string ToString() => Name;
}
3.3.5 Object Member Listing
object
class has many different members, not only GetType
and ToString
, here is all members
public class Object
{
public Object();
public extern Type GetType();
public virtual bool Equal(object obj);
public static bool Equals(object objA, object objB);
public static bool ReferenceEquals(object objA, object objB);
public virtual int GetHashCode();
public virtual string ToString();
protected virtual void Finalize();
protected extern object MemberwiseClone();
}
3.4 Structs
3.5 Access Modifiers
3.6 Interfaces
Interface = Similar as class, but only specifies behaviour and does not hold state (data)
- Interface only defines functions and has no fields
- Interface members (i.e. functions) are implicitly abstract
- Inheritance vs Interface:
- A class (or struct) can implement multiple interfaces.
- A class can only inherit from ONE class. Struct cannot inherit at all.
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Rest();
}
Declaration & Implementation of interface (e.g. shown above):
- Interface declaration provides no implementation of method, as they are implicitly abstract.
- Methods will be implemented by classes and structs
- An interface can only contain function (i.e. methods, properties, events, indexers, detail in chap 3.1)
- Interface members implicitly public and cannot declare an access modifier. Hence, class implementing an interface means all implemented methods are
public
(shown below)
internal class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext() => count-- > 0;
public object Current => count;
public void Reset() { throw new NotSupportedExceptions(); }
}
// public methods within internal class CountData can be called by casting it to the interface
public static class Util
{
public static object GetCountDown() => new CountDown();
}
IEnumerator e = (IEnumerator)Util.GetCountDown();
e.MoveNext(); // this won't work if IEnumerator is also internal
3.6.1 Extending an Interface
Interfaces can derive from other interfaces:
- extended (child) interface "inherits" all members of parent (these members also needed to be implemented)
e.g. extending interface
public interface IUndoable { void Undo(); }
public interface IRedoable : IUndoable { void Redo(); }
3.6.2 Explicit Interface Implementation
There 2 usage of Explicit Implementing interface member:
- resolve collision when implementing multiple interfaces
- Hide members that are highly specialized not for normal use case
Explicitly implemented member can be called by cast to its interface
interface I1 { void Foo(); }
interface I2 { int Foo(); }
// Explicit implementing interface
public class Widget : I1, I2
{
public void Foo()
{
Console.WriteLine("Widget's implementation of I1.Foo");
}
int I2.Foo()
// Explicitly implement I2's Foo method, to let 2 methods coexist in one class.
{
Console.WriteLine("Widgets's implementation of I2.Foo")
return 42;
}
}
// Call explicitly implemented member by casting to its interface
Widget w = new Widget();
w.Foo(); // Widget's implementation of I1.Foo
((I1)w).Foo(); // Widget's implementation of I1.Foo
((I2)w).Foo(); // Widget's implementation of I2.Foo
3.6.3 Implementing Interface Member Virtually
When a class implement interface, the implemented (implicitly) member is by default sealed
(refer chap 3.2.6). Hence the class member must be marked virtual
or abstract
to be further subclassed (overriden).
public interface IUndoable { void Undo(); }
public class TextBox : IUndaable
// Base class implementing interface
{
// implicitly implemented interface member will be overridden later
public virtual void Undo() => Console.WriteLine("TextBox.Undo");
}
public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// Calling interface member through base class or interface calls subclass'
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // RichTextBox.Undo
3.6.4 Reimplementing an Interface in a Subclass
- Even if base class has explicitly implemented member sealed, the subclass can reimplement (any) interface member.
- Reimplementation hijacks a member implementation. It works regardless member is
virtual
or not in base class.
e.g. Subclass reimplement interface
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
// Base class implicitly implement member
{
void IUndoable.Undo() => Console.WriteLine("TextBox.Undo");
}
// Subclass reimplement interface through interface calls
public class RichTextBox : TextBox, IUndoable
{
public void Undo() => Console.WriteLine("RichTextBox.Undo");
}
// Calling reimplemented member
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.UndoCase
((IUndoable)r).Undo(); // RichTextBox.UndoCase
Usage of reimplementation (TODO: Further understand the reason):
- Overriding explicitly implemnted interface members.
3.6.5 Interfaces and Boxing
3.6.6 Default Interface Members
3.7 Enums
3.8 Nested Types
3.9 Generics
C# has 2 separate mechanism for writing codes that reusable across different types:
- inheritance: express reusability with base type
- generic: express reusability with "template" that has "placeholder" types
Pro of Generic over inheritance:
- increase type safety
- reduce casting and boxing
3.9.1 Generic Types
How it works:
- Generic type is declared with type parameters (placeholder type to be filled in)
- Consumer of the generic type will supply the type argument
e.g. generic type Stack<T>
designed to stack instances of type T
// Define generic type Stack with single type parameter T
public class Stack<T>
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
3.9.2 Why Generic Exist
Generics make code reusable across different types.
Without Generic
To implement Stack<T>
for different types (int, string, etc) without generic, there are 2 ways:
- Hardcode separate version of class for every required element type. (i.e. create different concrete classes
IntStack
,StringStack
, etc.): it will create code duplication - Generalize by creating a concrete class using
object
as element type
// Generalized concrete class using object
public class ObjectStack
{
int position;
object[] data = new object[10];
public void Push (object obj) => data[position++] = obj;
public object Pop() => data[--position];
}
Problem of generalized class:
- Does not work as well as other specific classes
- Require boxing and downcasting which cannot be checked at compile tim
ObjectStack stack = new ObjectStack();
stackPush("s"); // Wrong type, but no error at runtime until now
int i = (int)stack.Pop(); // Downcast, create runtime error at this point
3.9.3 Generic Methods
Generic method declaration has type paramter within its signature
e.g. declare & call generic method
static void Swap<T> (ref T a, ref T b)
// type parameter within method signature
{
T temp = a;
a = b;
b = temp;
}
// Call generic method
int x = 5; int y = 10;
Swap(ref x, ref y);
// For more specific (no need to use type argument)
Swap<int> (ref x, ref y);
- Generally, there is no need to supply type arguments to a generic method, as compiler can implicitly infer the type.
How can a method be classed as generic?
- It must introduce type parameter (e.g.
<T>
) - Hence
Pop()
method inObjectStack
is not a generic method
Which construct can introduce type parameter <T>
?
- Can: Only in Methods and Types
- Cannot: Property, indexers, events, fields, constructor, operator, etc.
- They can only partake (参与) in any type parameter already declared by enclosing type (detailed in 3.9.4)
3.9.4 Declaring Type Parameters
Who can use type parameter for declaration
- Type parameters
<T>
can be introduced in declaration of classes, structs, interfaces, delegates, and methods. - Other construct (e.g. properties), cannot introduce/create a type parameter. They can use (partake) one.
public struct Nullable<T>
{
public T Value { get; } // property Value use type parameter
}
Multiple type parameter
A generic type or method can have multiple parameters:
class Dictionary<TKey, TValue> {...}
// Instantiate
Dictionary<int,string> myDict = new Dictionary<int,string> ();
// or
var myDict = new Dictionary<int, string>();
Overloaded generic methods
Generic type names and method names can be overloaded as long as number of type parameters is different.
class A {}
class A<T> {}
class A<T1,T2> {}
3.9.5 typeof
and Unbound Generic Types
- No open generic (e.g.
A<>
) at runtime: Open generic type do not exist at runtime, as they will be closed during compilation (e.g.A<int>
) - unbound generic at runtime: allowed purely as
Type
object- Specified by
typeof
operator - Multiple type args can be indicated by
,
- Specified by
class A<T> {}
class A<T1,T2> {}
Type a1 = typeof(A<>); // Unbound type (no type arguments in A<>)
Type a2 = typeof(A<,>); // Use commas to indicate multiple type arguments
// typeof can also specify closed type
Type a3 = type(A<int,int>);
3.9.6 The default Generic Value
- Keyword to get the default value for a generic type parameter:
default
- Default value for
- reference type:
null
- value type: result of bitwise-zeroing the value types' fields
- New feature in C#7.1, We can omit type argument for cases in which the compiler to infer
- reference type:
static void Zap<T> (T[] array)
{
for (int i = 0; i < array.Length; i++)
{
array[i] = default(T);
// Can be replaced by following
array[i] = default; // New feature in C#7.1
}
}
3.9.7 Generic Constraints
Constraint used to a type parameter (<T>
) to require more specific type argument
- Can be applied in definition or declaration
List of possible constraints:
- Base-class constraint:
where T : <base-class>
- specifies that type parameter must subclass a particular class
- Hence, implicitly convert the type to class
- Interface constraint:
where T : interface
- specifies the type paramter must implement interface
- Hence, implicitly convert the type to interface
- Reference-type constranit:
where T : class
T
must be reference type
- Nullable reference type:
where T : class?
- Value-type constraint:
where T : struct
- Unmanaged constraint (C# 7.3):
where T : unmanaged
- T must be a simple value type or a struct that is (recursively) free of any reference types.
- Parameterless constructor constraint:
where T : new()
- T must have a public parameterless constructor
- Naked type constraint:
where U : T
- Non-nullable value type or (from C#8):
where T : notnull
e.g. Generic Constraints
class SomeClass {}
interface Interface1 {}
class GenericClass<T,U> where T : SomeClass, Interface
where U : new()
// requires T to derive from SomecClass and implement Interface1, and requires U to provide parameterless constructor
{
...
}
e.g. base-class contraint and interface contraint
static T Max<T> (T a, T b) where T : IComparable<T>
{
return a.CompareTo (b) > 0 ? a : b;
}
int z = Max(5,10); // 10
string last = Max("ant", "zoo"); // zoo
e.g. parameterless constructor constraint
static void Intialize<T> (T[] array) where T : new()
{
for (int i = 0; i < array.Length; i++)
array[i] = new T();
}
e.g. naked type constraint
class Stack<T>
// method FilteredStack returns another Stack, containing only the sub
{
Stack<U> FilteredStack<U>() where U : T
{
...
}
}
TODO: Understand Naked type constraint (Not understood)
3.9.8 Subclassing Generic Types
a generic class can be further subclassed:
- type parameters in subclass can be closed or remain open
- subclass can add new type parameter
class Stack<T> {...}
// leave the base class's type parameter open
class SpecialStack<T> : Stack<T> {...}
// close the generic type parameter with concrete type
class IntStack : Stack<int> {...}
// Add new type arguments
class List<T> {...}
class KeyedList<T,TKey> : List<T> {...}
3.9.9 Self-Referencing Generic Declarations
A type can name itself as concrete type when closing a type argument
public interface IEquatable<T> { bool Equals (T obj); }
public class Balloon : IEquatable<Ballon>
// Ballon name itself as concrete type by closing type argument in IEquatable<>
{
public string Color { get; set; }
public int CC { get; set; }
public bool Equals(Ballon b)
{
if (b == null) return false;
return b.Color == Color && b.CC == CC;
}
}
3.9.10 Static Data
For each closed type, static data is unique
- Because, when a closed type (class) is created, its static data is then created along the closed type
- i.e. for the same closed type (i.e.
T
isint
), the static data is shared.
class Bob<T> { public static int Count; }
// For same closed type int, static data is unique
Console.WriteLine(++Bob<int>.Count); // 1
Console.WriteLine(++Bob<int>.Count); // 2
// For different closed type, static data is different
Console.WriteLine(++Bob<string>.Count); // 1
Console.WriteLine(++Bob<object>.Count);
3.9.11 Type Parameters and Conversions
TODO: Subchapter skipped
3.9.12 Covariance
TODO: Subchapter skipped
3.9.13 Contravariance
TODO: Subchapter skipped
3.9.14 C# Generics vs. C++ Templates
Common between C# Generics & C++ Templates:
- Placeholder type of producer filled by the consumer. (A synthesis relationship btw producer (generics & template) and consumer)
Diff:
- C# generics, producer types (e.g.
List<T>
) can be compiled into a library (mscorlib.dll
) standalone. Because synthesis btw producer & consumer happen in runtime, not during compilation. (i.e. comopiler does not close generics) - C++ templates, synthesis is performed at compile time. Hence, C++ don't deploy template libraries like
.dlls
. C++ template exists as source code.
e.g. Max method written in C++
template <clss T> T Max (T a, T b)
{
return a > b ? a : b;
}
- code will be compiled separately for each value of
T
- For a particular
T
it will take>
that's available to theT
- Compilation will fail if a
T
does not support>
operator
e.g. Max method written in C#
static T Max<T> (T a, T b) where T : IComparable<T> => a.CompareTo(b) > 0 ? a : b;
Max
need to be compiled oncwe and work for all possible value ofC
e.g. Wrong implementation in C#
static T Max <T> (T a, T b) => (a > b ? a : b);
- Compilation fail as there is no single meaning for
>
across allT
, as not everyT
has>
operator