C#
Version 2.0 Specification
September 2005
Notice
© 2005 Microsoft Corporation. All rights reserved.
Microsoft, Windows, Visual Basic, Visual C#, and Visual C++ are either registered trademarks or trademarks of Microsoft Corporation in the
Other product and company names mentioned herein may be the trademarks of their respective owners.
Table of Contents
19. Introduction to C# 2.0........................................................................................................................ 1
19.1 Generics........................................................................................................................................... 1
19.1.1 Why generics?............................................................................................................................ 1
19.1.2 Creating and using generics.......................................................................................................... 2
19.1.3 Generic type instantiations............................................................................................................ 3
19.1.4 Constraints.................................................................................................................................. 4
19.1.5 Generic methods......................................................................................................................... 5
19.2 Anonymous methods.......................................................................................................................... 6
19.2.1 Method group conversions........................................................................................................... 8
19.3 Iterators............................................................................................................................................ 8
19.4 Partial types.................................................................................................................................... 11
19.5 Nullable types.................................................................................................................................. 12
20. Generics............................................................................................................................................ 15
20.1 Generic class declarations................................................................................................................ 15
20.1.1 Type parameters....................................................................................................................... 15
20.1.2 The instance type...................................................................................................................... 16
20.1.3 Base specification...................................................................................................................... 17
20.1.4 Members of generic classes....................................................................................................... 17
20.1.5 Static fields in generic classes..................................................................................................... 18
20.1.6 Static constructors in generic classes.......................................................................................... 19
20.1.7 Accessing protected members.................................................................................................... 19
20.1.8 Overloading in generic classes.................................................................................................... 20
20.1.9 Parameter array methods and type parameters............................................................................ 20
20.1.10 Overriding and generic classes.................................................................................................. 20
20.1.11 Operators in generic classes..................................................................................................... 21
20.1.12 Nested types in generic classes................................................................................................. 22
20.1.13 Application entry point.............................................................................................................. 23
20.2 Generic struct declarations............................................................................................................... 23
20.3 Generic interface declarations........................................................................................................... 23
20.3.1 Uniqueness of implemented interfaces........................................................................................ 24
20.3.2 Explicit interface member implementations.................................................................................. 24
20.4 Generic delegate declarations........................................................................................................... 25
20.5 Constructed types............................................................................................................................ 26
20.5.1 Type arguments......................................................................................................................... 26
20.5.2 Open and closed types............................................................................................................... 27
20.5.3 Base classes and interfaces of a constructed type........................................................................ 27
20.5.4 Members of a constructed type.................................................................................................. 28
20.5.5 Accessibility of a constructed type.............................................................................................. 29
20.5.6 Conversions.............................................................................................................................. 29
20.5.7 Using alias directives................................................................................................................. 29
20.5.8 Attributes.................................................................................................................................. 30
20.5.9 Arrays and the generic IList interface......................................................................................... 30
20.6 Generic methods.............................................................................................................................. 31
20.6.1 Generic method signatures......................................................................................................... 31
20.6.2 Virtual generic methods............................................................................................................. 32
20.6.3 Calling generic methods............................................................................................................. 33
20.6.4 Inference of type arguments....................................................................................................... 34
20.6.5 Grammar ambiguities................................................................................................................. 35
20.6.6 Using a generic method with a delegate...................................................................................... 36
20.6.7 Members that cannot be generic................................................................................................. 36
20.7 Constraints...................................................................................................................................... 37
20.7.1 Satisfying constraints................................................................................................................. 41
20.7.2 Member lookup on type parameters............................................................................................ 41
20.7.3 Type parameters and boxing....................................................................................................... 42
20.7.4 Conversions involving type parameters........................................................................................ 43
20.8 Expressions and statements.............................................................................................................. 45
20.8.1 Object creation expressions........................................................................................................ 45
20.8.2 The typeof operator................................................................................................................... 45
20.8.3 Reference equality operators...................................................................................................... 46
20.8.4 The is operator.......................................................................................................................... 47
20.8.5 The as operator......................................................................................................................... 47
20.8.6 Exception statements................................................................................................................. 47
20.8.7 The lock statement.................................................................................................................... 47
20.8.8 The using statement................................................................................................................... 47
20.8.9 The foreach statement............................................................................................................... 47
20.9 Revised lookup rules........................................................................................................................ 48
20.9.1 Namespace and type names....................................................................................................... 48
20.9.2 Member lookup......................................................................................................................... 50
20.9.3 Applicable function member....................................................................................................... 51
20.9.4 Better function member............................................................................................................. 51
20.9.5 Simple names............................................................................................................................ 52
20.9.6 Member access......................................................................................................................... 53
20.9.7 Method invocations.................................................................................................................... 55
20.10 Right-shift grammar changes.......................................................................................................... 56
21. Anonymous methods........................................................................................................................ 59
21.1 Anonymous method expressions....................................................................................................... 59
21.2 Anonymous method signatures.......................................................................................................... 59
21.3 Anonymous method conversions....................................................................................................... 59
21.4 Anonymous method blocks............................................................................................................... 61
21.5 Outer variables................................................................................................................................ 61
21.5.1 Captured outer variables............................................................................................................ 61
21.5.2 Instantiation of local variables..................................................................................................... 62
21.6 Anonymous method evaluation.......................................................................................................... 64
21.7 Delegate instance equality................................................................................................................ 65
21.8 Definite assignment.......................................................................................................................... 65
21.9 Method group conversions................................................................................................................ 65
21.10 Delegate creation expressions......................................................................................................... 67
21.11 Implementation example................................................................................................................. 67
22. Iterators............................................................................................................................................ 71
22.1 Iterator blocks................................................................................................................................. 71
22.1.1 Enumerator interfaces................................................................................................................ 71
22.1.2 Enumerable interfaces............................................................................................................... 71
22.1.3 Yield type................................................................................................................................. 71
22.1.4 This access............................................................................................................................... 72
22.2 Enumerator objects.......................................................................................................................... 72
22.2.1 The MoveNext method.............................................................................................................. 72
22.2.2 The Current property................................................................................................................. 73
22.2.3 The Dispose method.................................................................................................................. 74
22.3 Enumerable objects.......................................................................................................................... 74
22.3.1 The GetEnumerator method....................................................................................................... 74
22.4 The yield statement.......................................................................................................................... 75
22.4.1 Definite assignment................................................................................................................... 76
22.5 Implementation example................................................................................................................... 76
23. Partial types...................................................................................................................................... 83
23.1 Partial declarations........................................................................................................................... 83
23.1.1 Attributes.................................................................................................................................. 83
23.1.2 Modifiers.................................................................................................................................. 84
23.1.3 Type parameters and constraints................................................................................................ 84
23.1.4 Base class................................................................................................................................. 84
23.1.5 Base interfaces......................................................................................................................... 85
23.1.6 Members.................................................................................................................................. 85
23.2 Name binding.................................................................................................................................. 86
24. Nullable types................................................................................................................................... 87
24.1 Nullable types.................................................................................................................................. 87
24.1.1 Members.................................................................................................................................. 87
24.1.2 Default value............................................................................................................................. 88
24.1.3 The value type constraint........................................................................................................... 88
24.2 Conversions..................................................................................................................................... 88
24.2.1 Null literal conversions............................................................................................................... 88
24.2.2 Nullable conversions.................................................................................................................. 88
24.2.3 Boxing and unboxing conversions................................................................................................ 89
24.2.4 Permitted user-defined conversions............................................................................................. 90
24.2.5 Evaluation of user-defined conversions........................................................................................ 90
24.2.6 Lifted conversions..................................................................................................................... 90
24.2.7 User-defined implicit conversions................................................................................................ 90
24.2.8 User-defined explicit conversions................................................................................................ 91
24.3 Expressions..................................................................................................................................... 92
24.3.1 Lifted operators......................................................................................................................... 92
24.3.2 Permitted user-defined operators................................................................................................ 93
24.3.3 Operator overload resolution....................................................................................................... 93
24.3.4 Equality operators and null.......................................................................................................... 93
24.3.5 The is operator.......................................................................................................................... 94
24.3.6 The as operator......................................................................................................................... 94
24.3.7 Compound assignment............................................................................................................... 94
24.3.8 The bool? type........................................................................................................................... 95
24.3.9 The null coalescing operator....................................................................................................... 95
25. Other features................................................................................................................................... 97
25.1 Property accessor accessibility......................................................................................................... 97
25.1.1 Accessor declarations................................................................................................................ 97
25.1.2 Accessor usage......................................................................................................................... 98
25.1.3 Overriding and interface implementation...................................................................................... 98
25.2 Static classes................................................................................................................................... 99
25.2.1 Static class declarations............................................................................................................. 99
25.2.2 Referencing static class types................................................................................................... 100
25.3 Namespace alias qualifiers.............................................................................................................. 100
25.3.1 Qualified alias member............................................................................................................. 102
25.3.2 Uniqueness of aliases............................................................................................................... 103
25.4 Extern aliases................................................................................................................................ 104
25.4.1 Extern alias directives.............................................................................................................. 105
25.5 Pragma directives.......................................................................................................................... 106
25.5.1 Pragma warning...................................................................................................................... 107
25.6 Default value expression................................................................................................................. 107
25.7 Conditional attribute classes............................................................................................................ 108
25.8 Fixed size buffers........................................................................................................................... 109
25.8.1 Fixed size buffer declarations.................................................................................................... 109
25.8.2 Fixed size buffers in expressions............................................................................................... 110
25.8.3 Fixed statements...................................................................................................................... 111
25.8.4 Definite assignment checking.................................................................................................... 111
19. Introduction to C# 2.0
C# 2.0 introduces several language extensions, including Generics, Anonymous Methods, Iterators, Partial Types, and Nullable Types.
· Generics permit classes, structs, interfaces, delegates, and methods to be parameterized by the types of data they store and manipulate. Generics are useful because they provide stronger compile-time type checking, require fewer explicit conversions between data types, and reduce the need for boxing operations and run-time type checks.
· Anonymous methods allow code blocks to be written “in-line” where delegate values are expected. Anonymous methods are similar to lambda functions in the Lisp programming language. C# 2.0 supports the creation of “closures” where anonymous methods access surrounding local variables and parameters.
· Iterators are methods that incrementally compute and yield a sequence of values. Iterators make it easy for a type to specify how the foreach statement will iterate over its elements.
· Partial types allow classes, structs, and interfaces to be broken into multiple pieces stored in different source files for easier development and maintenance. Additionally, partial types allow separation of machine-generated and user-written parts of types so that it is easier to augment code generated by a tool.
· Nullable types represent values that possibly are unknown. A nullable type supports all values of its underlying type plus an additional null state. Any value type can be the underlying type of a nullable type. A nullable type supports the same conversions and operators as its underlying type, but additionally provides null value propagation similar to SQL.
This chapter gives an introduction to these new features. Following the introduction are five chapters that provide a complete technical specification of the features. The final chapter describes a number of smaller extensions that are also included in C# 2.0.
The language extensions in C# 2.0 were designed to ensure maximum compatibility with existing code. For example, even though C# 2.0 gives special meaning to the words where, yield, and partial in certain contexts, these words can still be used as identifiers. Indeed, C# 2.0 adds no new keywords as such keywords could conflict with identifiers in existing code.
For the latest information on the C# language and instructions for providing feedback on this document, please visit the C# Language Home Page (http://msdn.microsoft.com/vcsharp/language).
19.1 Generics
Generics permit classes, structs, interfaces, delegates, and methods to be parameterized by the types of data they store and manipulate. C# generics will be immediately familiar to users of generics in Eiffel or
19.1.1 Why generics?
Without generics, general purpose data structures can use type object to store data of any type. For example, the following simple Stack class stores its data in an object array, and its two methods, Push and Pop, use object to accept and return data, respectively:
public class Stack
{
object[] items;
int count;
public void Push(object item) {...}
public object Pop() {...}
}
While the use of type object makes the Stack class very flexible, it is not without drawbacks. For example, it is possible to push a value of any type, such a Customer instance, onto a stack. However, when a value is retrieved, the result of the Pop method must explicitly be cast back to the appropriate type, which is tedious to write and carries a performance penalty for run-time type checking:
Stack stack = new Stack();
stack.Push(new Customer());
Customer c = (Customer)stack.Pop();
If a value of a value type, such as an int, is passed to the Push method, it is automatically boxed. When the int is later retrieved, it must be unboxed with an explicit type cast:
Stack stack = new Stack();
stack.Push(3);
int i = (int)stack.Pop();
Such boxing and unboxing operations add performance overhead since they involve dynamic memory allocations and run-time type checks.
A further issue with the Stack class is that it is not possible to enforce the kind of data placed on a stack. Indeed, a Customer instance can be pushed on a stack and then accidentally cast it to the wrong type after it is retrieved:
Stack stack = new Stack();
stack.Push(new Customer());
string s = (string)stack.Pop();
While the code above is an improper use of the Stack class, the code is technically speaking correct and a compile-time error is not reported. The problem does not become apparent until the code is executed, at which point an InvalidCastException is thrown.
The Stack class would clearly benefit from the ability to specify its element type. With generics, that becomes possible.
19.1.2 Creating and using generics
Generics provide a facility for creating types that have type parameters. The example below declares a generic Stack class with a type parameter T. The type parameter is specified in < and >
public class Stack<T>
{
T[] items;
int count;
public void Push(T item) {...}
public T Pop() {...}
}
When the generic class Stack<T> is used, the actual type to substitute for T is specified. In the following example, int is given as the type argument for T:
Stack<int> stack = new Stack<int>();
stack.Push(3);
int x = stack.Pop();
The Stack<int> type is called a constructed type. In the Stack<int> type, every occurrence of T is replaced with the type argument int. When an instance of Stack<int> is created, the native storage of the items array is an int[] rather than object[], providing substantial storage efficiency compared to the non-generic Stack. Likewise, the Push and Pop methods of a Stack<int> operate on int values, making it a compile-time error to push values of other types onto the stack, and eliminating the need to explicitly cast values back to their original type when they’re retrieved.
Generics provide strong typing, meaning for example that it is an error to push an int onto a stack of Customer objects. Just as a Stack<int> is restricted to operate only on int values, so is Stack<Customer> restricted to Customer objects, and the compiler will report errors on the last two lines of the following example:
Stack<Customer> stack = new Stack<Customer>();
stack.Push(new Customer());
Customer c = stack.Pop();
stack.Push(3); // Type mismatch error
int x = stack.Pop(); // Type mismatch error
Generic type declarations can have any number of type parameters. The Stack<T> example above has only one type parameter, but a generic Dictionary class might have two type parameters, one for the type of the keys and one for the type of the values:
public class Dictionary<K,V>
{
public void Add(K key, V value) {...}
public V this[K key] {...}
}
When Dictionary<K,V> is used, two type arguments would have to be supplied:
Dictionary<string,Customer> dict = new Dictionary<string,Customer>();
dict.Add("Peter", new Customer());
Customer c = dict["Peter"];
19.1.3 Generic type instantiations
Similar to a non-generic type, the compiled representation of a generic type is intermediate language (IL) instructions and metadata. The representation of the generic type of course also encodes the existence and use of type parameters.
The first time an application creates an instance of a constructed generic type, such as Stack<int>, the just-in-time (JIT) compiler of the .NET Common Language Runtime converts the generic IL and metadata to native code, substituting actual types for type parameters in the process. Subsequent references to that constructed generic type then use the same native code. The process of creating a specific constructed type from a generic type is known as a generic type instantiation.
The .NET Common Language Runtime creates a specialized copy of the native code for each generic type instantiation with a value type, but shares a single copy of the native code for all reference types (since, at the native code level, references are just pointers with the same representation).
19.1.4 Constraints
Commonly, a generic class will do more than just store data based on a type parameter. Often, the generic class will want to invoke methods on objects whose type is given by a type parameter. For example, an Add method in a Dictionary<K,V> class might need to compare keys using a CompareTo method:
public class Dictionary<K,V>
{
public void Add(K key, V value)
{
...
if (key.CompareTo(x) < 0) {...} // Error, no CompareTo method
...
}
}
Since the type argument specified for K could be any type, the only members that can be assumed to exist on the key parameter are those declared by type object, such as Equals, GetHashCode, and ToString; a compile-time error therefore occurs in the example above. It is of course possible to cast the key parameter to a type that contains a CompareTo method. For example, the key parameter could be cast to IComparable:
public class Dictionary<K,V>
{
public void Add(K key, V value)
{
...
if (((IComparable)key).CompareTo(x) < 0) {...}
...
}
}
While this solution works, it requires a dynamic type check at run-time, which adds overhead. It furthermore defers error reporting to run-time, throwing an InvalidCastException if a key doesn’t implement IComparable.
To provide stronger compile-time type checking and reduce type casts, C# permits an optional list of constraints to be supplied for each type parameter. A type parameter constraint specifies a requirement that a type must fulfill in order to be used as an argument for that type parameter. Constraints are declared using the word where, followed by a type parameter and a colon, followed by a comma-separated list of class types, interface types, and type parameters (or the special reference type, value type, and constructor constraints).
In order for the Dictionary<K,V> class to ensure that keys always implement IComparable, the class declaration can specify a constraint for the type parameter K:
public class Dictionary<K,V> where K: IComparable
{
public void Add(K key, V value)
{
...
if (key.CompareTo(x) < 0) {...}
...
}
}
Given this declaration the compiler will ensure that any type argument supplied for K is a type that implements IComparable. Furthermore, it is no longer necessary to explicitly cast the key parameter to IComparable before calling the CompareTo method; all members of a type given as a constraint for a type parameter are directly available on values of that type parameter type.
For a given type parameter, it is possible to specify any number of interfaces and type parameters as constraints, but no more than one class. Each constrained type parameter has a separate where clause. In the example below, the type parameter K has two interface constraints, while the type parameter E has a class type constraint and a constructor constraint:
public class EntityTable<K,E>
where K: IComparable<K>, IPersistable
where E: Entity, new()
{
public void Add(K key, E entity)
{
...
if (key.CompareTo(x) < 0) {...}
...
}
}
The constructor constraint, new(), in the example above ensures that a type used as a type argument for E has a public, parameterless constructor, and it permits the generic class to use new E() to create instances of that type.
Type parameter constrains should be used with care. While they provide stronger compile-time type checking and in some cases improve performance, they also restrict the possible uses of a generic type. For example, a generic class List<T> might constrain T to implement IComparable such that the list’s Sort method can compare items. However, doing so would preclude use of List<T> for types that don’t implement IComparable, even if the Sort method is
19.1.5 Generic methods
In some cases a type parameter is not needed for an entire class, but only inside a particular method. Often, this occurs when creating a method that takes a generic type as a parameter. For example, when using the Stack<T> class described earlier, a common pattern might be to push multiple values in a row, and it might be convenient to write a method that does so in a single call. For a particular constructed type, such as Stack<int>, the method would look like this:
void PushMultiple(Stack<int> stack, params int[] values) {
foreach (int value in values) stack.Push(value);
}
This method can be used to push multiple int values onto a Stack<int>:
Stack<int> stack = new Stack<int>();
PushMultiple(stack, 1, 2, 3, 4);
However, the method above only works with the particular constructed type Stack<int>. To have it work with any Stack<T>, the method must be written as a generic method. A generic method has one or more type parameters specified in < and >
void PushMultiple<T>(Stack<T> stack, params T[] values) {
foreach (T value in values) stack.Push(value);
}
Using this generic method, it is possible to push multiple items onto any Stack<T>. When calling a generic method, type arguments are given in angle brackets in the method invocation. For example:
Stack<int> stack = new Stack<int>();
PushMultiple<int>(stack, 1, 2, 3, 4);
This generic PushMultiple method is more reusable than the previous version, since it works on any Stack<T>, but it appears to be less convenient to call, since the desired T must be supplied as a type argument to the method. In many cases, however, the compiler can deduce the correct type argument from the other arguments passed to the method, using a process called type inferencing. In the example above, since the first regular argument is of type Stack<int>, and the subsequent arguments are of type int, the compiler can reason that the type parameter must be int. Thus, the generic PushMultiple method can be called without specifying the type parameter:
Stack<int> stack = new Stack<int>();
PushMultiple(stack, 1, 2, 3, 4);
19.2 Anonymous methods
Event handlers and other callbacks are often invoked exclusively through delegates and never directly. Even so, it has thus far been necessary to place the code of event handlers and callbacks in distinct methods to which
The following example shows a simple input form that contains a list box, a text box, and a button. When the button is clicked, an item containing the text in the text box is added to the list box.
class InputForm: Form
{
ListBox listBox;
TextBox textBox;
Button addButton;
public MyForm() {
listBox = new ListBox(...);
textBox = new TextBox(...);
addButton = new Button(...);
addButton.Click += new EventHandler(AddClick);
}
void AddClick(object sender, EventArgs e) {
listBox.Items.Add(textBox.Text);
}
}
Even though only a single statement is executed in response to the button’s Click event, that statement must be extracted into a separate method with a full parameter list, and an EventHandler delegate referencing that method must be manually created. Using an anonymous method, the event handling code becomes significantly more succinct:
class InputForm: Form
{
ListBox listBox;
TextBox textBox;
Button addButton;
public MyForm() {
listBox = new ListBox(...);
textBox = new TextBox(...);
addButton = new Button(...);
addButton.Click += delegate {
listBox.Items.Add(textBox.Text);
};
}
}
An anonymous method consists of the keyword delegate, an optional parameter list, and a statement list enclosed in { and }
addButton.Click += delegate(object sender, EventArgs e) {
MessageBox.Show(((Button)sender).Text);
};
In the previous examples, an implicit conversion occurs from the anonymous method to the EventHandler delegate type (the type of the Click event). This implict conversion is possible because the parameter list and return type of the
· The parameter list of a
o The anonymous method has no parameter list and the delegate has no out parameters.
o The anonymous method includes a parameter list that exactly matches the delegate’s parameters in number, types, and modifiers.
· The return type of a
o The
o The delegate’s return type is not void and the expressions associated with all return statements in the anonymous method can be implicitly converted to the return type of the delegate.
Both the parameter list and the return type of a delegate must be compatible with an anonymous method before an implicit conversion to that
The following example uses anonymous methods to write functions “in-line.” The anonymous methods are passed as parameters of a Function delegate type.
using System;
delegate double Function(double x);
class Test
{
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
static double[] MultiplyAllBy(double[] a, double factor) {
return Apply(a, delegate(double x) { return x * factor; });
}
static void
double[] a = {0.0, 0.5, 1.0};
double[] squares = Apply(a, delegate(double x) { return x * x; });
double[] doubles = MultiplyAllBy(a, 2.0);
}
}
The Apply method applies a given Function to the elements of a double[], returning a double[] with the results. In the Main method, the second parameter passed to Apply is an anonymous method that is compatible with the Function delegate type. The anonymous method simply returns the square of its argument, and thus the result of that Apply invocation is a double[] containing the squares of the values in a.
The MultiplyAllBy method returns a double[] created by multiplying each of the values in the argument array a by a given factor. In order to produce its result, MultiplyAllBy invokes the Apply method, passing an anonymous method that multiplies the argument x by factor.
Local variables and parameters whose scope contains an anonymous method are called outer variables of the anonymous method. In the MultiplyAllBy method, a and factor are outer variables of the anonymous method passed to Apply, and because the anonymous method references factor, factor is said to have been captured by the anonymous method. Ordinarily, the lifetime of a local variable is limited to execution of the block or statement with which it is associated. However, the lifetime of a captured outer variable is extended at least until the delegate referring to the anonymous method becomes eligible for garbage collection.
19.2.1 Method group conversions
As described in the previous section, an anonymous method can be implicitly converted to a compatible
addButton.Click += new EventHandler(AddClick);
Apply(a, new Function(Math.Sin));
can instead be written
addButton.Click += AddClick;
Apply(a, Math.Sin);
When the shorter form is used, the compiler automatically infers which delegate type to instantiate, but the effects are otherwise the same as the longer form.
19.3 Iterators
The C# foreach statement is used to iterate over the elements of an enumerable collection. In order to be enumerable, a collection must have a parameterless GetEnumerator method that returns an enumerator. Generally, enumerators are difficult to implement, but the task is significantly simplified with iterators.
An iterator is a statement block that yields an ordered sequence of values. An iterator is distinguished from a normal statement block by the presence of one or more yield statements:
· The yield return statement produces the next value of the iteration.
· The yield break statement
An iterator can be used as the body of a function member as long as the return type of the function member is one of the enumerator interfaces or one of the enumerable interfaces:
· The enumerator interfaces are System.Collections.IEnumerator and types constructed from System.Collections.Generic.IEnumerator<T>.
· The enumerable interfaces are System.Collections.IEnumerable and types constructed from System.Collections.Generic.IEnumerable<T>.
It is important to understand that an iterator is not a kind of member, but is a means of implementing a function member. A member implemented via an iterator can be overridden or overloaded by other members which may or may not be implemented with iterators.
The following Stack<T> class implements its GetEnumerator method using an iterator. The iterator enumerates the elements of the stack in top to bottom order.
using System.Collections.Generic;
public class Stack<T>: IEnumerable<T>
{
T[] items;
int count;
public void Push(T data) {...}
public T Pop() {...}
public IEnumerator<T> GetEnumerator() {
for (int i = count – 1; i >= 0; --i) {
yield return items[i];
}
}
}
The presence of the GetEnumerator method makes Stack<T> an enumerable type, allowing instances of Stack<T> to be used in a foreach statement. The following example pushes the values 0 through 9 onto an integer stack and then uses a foreach loop to display the values in top to bottom order.
using System;
class Test
{
static void
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack) Console.Write("{0} ", i);
Console.WriteLine();
}
}
The output of the example is:
9 8 7 6 5 4 3 2 1 0
The foreach statement implicitly calls a collection’s parameterless GetEnumerator method to obtain an enumerator. There can only be one such parameterless GetEnumerator method defined by a collection, yet it is often appropriate to have multiple ways of enumerating, and ways of controlling the enumeration through parameters. In such cases, a collection can use iterators to implement properties or methods that return one of the enumerable interfaces. For example, Stack<T> might introduce two new properties, TopToBottom and BottomToTop, of type IEnumerable<T>:
using System.Collections.Generic;
public class Stack<T>: IEnumerable<T>
{
T[] items;
int count;
public void Push(T data) {...}
public T Pop() {...}
public IEnumerator<T> GetEnumerator() {
for (int i = count – 1; i >= 0; --i) {
yield return items[i];
}
}
public IEnumerable<T> TopToBottom {
get {
return this;
}
}
public IEnumerable<T> BottomToTop {
get {
for (int i = 0; i < count; i++) {
yield return items[i];
}
}
}
}
The get accessor for the TopToBottom property just returns this since the stack itself is an enumerable. The BottomToTop property returns an enumerable implemented with a C# iterator. The following example shows how the properties can be used to enumerate stack elements in either order:
using System;
class Test
{
static void
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
foreach (int i in stack.TopToBottom) Console.Write("{0} ", i);
Console.WriteLine();
foreach (int i in stack.BottomToTop) Console.Write("{0} ", i);
Console.WriteLine();
}
}
Of course, these properties can be used outside of a foreach statement as well. The following example passes the results of invoking the properties to a separate Print method. The example also shows an iterator used as the body of a FromToBy method that takes parameters:
using System;
using System.Collections.Generic;
class Test
{
static void Print(IEnumerable<int> collection) {
foreach (int i in collection) Console.Write("{0} ", i);
Console.WriteLine();
}
static IEnumerable<int> FromToBy(int from, int to, int by) {
for (int i = from; i <= to; i += by) {
yield return i;
}
}
static void
Stack<int> stack = new Stack<int>();
for (int i = 0; i < 10; i++) stack.Push(i);
Print(stack.TopToBottom);
Print(stack.BottomToTop);
Print(FromToBy(10, 20, 2));
}
}
The output of the example is:
9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
10 12 14 16 18 20
The generic and non-generic enumerable interfaces contain a single member, a GetEnumerator method that takes no arguments and returns an enumerator interface. An enumerable acts as an enumerator factory. Properly implemented enumerables generate independent enumerators each time their GetEnumerator method is called. Assuming the internal state of the enumerable has not changed between two calls to GetEnumerator, the two enumerators returned should produce the same set of values in the same order. This should hold even if the lifetime of the enumerators overlap as in the following code sample:
using System;
using System.Collections.Generic;
class Test
{
static IEnumerable<int> FromTo(int from, int to) {
while (from <= to) yield return from++;
}
static void
IEnumerable<int> e = FromTo(1, 10);
foreach (int x in e) {
foreach (int y in e) {
Console.Write("{0,3} ", x * y);
}
Console.WriteLine();
}
}
}
The code above prints a simple multiplication table of the integers 1 through 10. Note that the FromTo method is invoked only once to generate the enumerable e. However, e.GetEnumerator() is invoked multiple times (by the foreach statements) to generate multiple equivalent enumerators. These enumerators all encapsulate the iterator code specified in the declaration of FromTo. Note that the iterator code modifies the from parameter. Nevertheless, the enumerators act independently because each enumerator is given its own copy of the from and to parameters. The sharing of transient state between enumerators is one of several common subtle flaws that should be avoided when implementing enumerables and enumerators. C# iterators are designed to help avoid these problems and to implement robust enumerables and enumerators in a simple intuitive way.
19.4 Partial types
While it is good programming practice to maintain all source code for a type in a single file, sometimes a type becomes large enough that this is an impractical constraint.
Partial types allow classes, structs, and interfaces to be broken into multiple pieces stored in different source files for easier development and maintenance. Additionally, partial types allow separation of machine-generated and user-written parts of types so that it is easier to augment code generated by a tool.
A new type modifier, partial, is used when defining a type in multiple parts. The following is an example of a partial class that is implemented in two parts. The two parts may be in different source files, for example because the first part is machine generated by a database mapping tool and the second part is manually authored:
public partial class Customer
{
private int id;
private string name;
private string address;
private List<Order> orders;
public Customer() {
...
}
}
public partial class Customer
{
public void SubmitOrder(Order order) {
orders.Add(order);
}
public bool HasOutstandingOrders() {
return orders.Count > 0;
}
}
When the two parts above are compiled together, the resulting code is the same as if the class had been written as a single unit:
public class Customer
{
private int id;
private string name;
private string address;
private List<Order> orders;
public Customer() {
...
}
public void SubmitOrder(Order order) {
orders.Add(order);
}
public bool HasOutstandingOrders() {
return orders.Count > 0;
}
}
All parts of a partial type must be compiled together such that the parts can be merged at compile-time. Partial types specifically do not allow already compiled types to be extended.
19.5 Nullable types
Support for nullability across all types, including value types, is essential when interacting with databases, yet general purpose programming languages have historically provided little or no support in this area. Many approaches exist for handling nulls and value types without direct language support, but all have shortcomings. For example, one approach is to use a “special” value (such as −1 for integers) to indicate null, but this only works when an unused value can be identified. Another approach is to maintain boolean null indicators in separate fields or variables, but this doesn’t work well for parameters and return values. A third approach is to use a set of user-defined nullable types, but this only works for a closed set of types. C#’s nullable types solve this long standing problem by providing complete and integrated support for nullable forms of all value types.
Nullable types are constructed using the ? type modifier. For example, int? is the nullable form of the predefined type int. A nullable type’s underlying type must be a non-nullable value type.
A nullable type is a structure that combines a value of the underlying type with a boolean null
An implicit conversion exists from any non-nullable value type to a nullable form of that type.
int? x = 123;
int? y = null;
if (x.HasValue) Console.WriteLine(x.Value);
if (y.HasValue) Console.WriteLine(y.Value);
the int value 123 and the null literal are implicitly converted to the nullable type int?. The example outputs 123 for x, but the second Console.WriteLine isn’t executed because y.HasValue is false.
Nullable conversions and lifted conversions permit predefined and user-defined conversions that operate on non-nullable value types to also be used with nullable forms of those types. Likewise, lifted operators permit predefined and user-defined operators that work for non-nullable value types also work for nullable forms of those types.
For every predefined conversion from a non-nullable value type S to a non-nullable value type T, a predefined nullable conversion automatically exists from S? to T?. This nullable conversion is a null propagating form of the underlying conversion: It converts a null source value directly to a null target value, but otherwise performs the underlying non-nullable conversion. Nullable conversions are furthermore provided from S to T? and from S? to T, the latter as an explicit conversion that throws an exception if the source value is null.
Some examples of nullable conversions are shown in the following.
int i = 123;
int? x = i; // int --> int?
double? y = x; // int? --> double?
int? z = (int?)y; // double? --> int?
int j = (int)z; // int? --> int
A user-defined conversion operator has a lifted form when the source and target types are both non-nullable value types. A ? modifier is added to the the source and target types to create the lifted form. Similar to predefined nullable conversions, lifted conversion operators propagate nulls.
A non-comparison operator has a lifted form when the operand types and result type are all non-nullable value types. For non-comparison operators, a ? modifier is added to each operand type and the result type to create the lifted form. For example, the lifted form of the predefined + operator that takes two int operands and returns an int is an operator that takes two int? operands and returns an int?. Similar to lifted conversions, lifted non-comparison operators are null propagating: If either operand of a lifted operator is null, the result is null.
The following example uses a lifted + operator to add two int? values:
int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x + y;
the assignment to z effectively corresponds to:
int? z = x.HasValue && y.HasValue ? x.Value + y.Value : (int?)null;
Because an implicit conversion exists from a non-nullable value type to its nullable form, a lifted operator is applicable when just one operand is of a nullable type. The following example uses the same lifted + operator as the example above:
int? x = GetNullableInt();
int? y = x + 1;
If x is null, y is assigned null. Otherwise, y is assigned the value of x plus one.
The null propagating semantics of C#’s nullable conversions, lifted conversions, and lifted non-comparison operators are very similar to the corresponding conversions and operators in SQL. However, C#’s lifted comparison operators produce regular boolean results rather than introducing SQL’s three-valued boolean logic.
A comparison operator (==, !=, <, >, <=, >=) has a lifted form when the operand types are both non-nullable value types and the result type is bool. The lifted form of a comparison operator is formed by adding a ? modifier to each operand type (but not to the result type). Lifted forms of the == and != operators consider two null values equal, and a null value unequal to a non-null value. Lifted forms of the <, >, <=, and >= operators return false if one or both operands are null.
When one of the operands of the == or != operator is the null literal, the other operand may be of any nullable type regardless of whether the underlying value type actually declares that operator. In cases where no operator == or != implementation is available, a check of the operand’s HasValue property is substituted. The effect of this rule is that statements such as
if (x == null) Console.WriteLine("x is null");
if (x != null) Console.WriteLine("x is non-null");
are permitted for an x of any nullable type or reference type, thus providing a common way of performing null checks for all types that can be null.
A new null coalescing operator, ??, is provided. The result of a ?? b is a if a is non-null; otherwise, the result is b. Intuitively, b supplies the value to use when a is null.
When a is of a nullable type and b is of a non-nullable type, a ?? b returns a non-nullable value, provided the appropriate implicit conversions exist between the operand types. In the example
int? x = GetNullableInt();
int? y = GetNullableInt();
int? z = x ?? y;
int i = z ?? -1;
the type of x ?? y is int?, but the type of z ?? -1 is int. The latter operation is particularly convenient because it removes the ? from the type and at the same time supplies the default value to use in the null case.
The null coalescing operator also works for reference types. The example
string s = GetStringValue();
Console.WriteLine(s ?? "Unspecified");
outputs the value of s, or outputs Unspecified if s is null.
20. Generics
20.1 Generic class declarations
A generic class declaration is a declaration of a class that requires type arguments to be supplied in order to form actual types.
A class declaration can optionally define type parameters:
class-declaration:
attributesopt class-modifiersopt class identifier type-parameter-listopt class-baseopt
type-parameter-constraints-clausesopt class-body ;opt
A class declaration cannot supply type-parameter-constraints-clauses (§20.7) unless it also supplies a type-parameter-list.
A class declaration that supplies a type-parameter-list is a generic class declaration. Additionally, any class nested inside a generic class declaration or a generic struct declaration is itself a generic class declaration, since type parameters for the containing type must be supplied to create a constructed type.
Generic class declarations follow the same rules as non-generic class declarations except where noted. Generic class declarations can be nested inside non-generic class declarations.
A generic class is referenced using a constructed type (§20.4). Given the generic class declaration
class List<T> {}
some examples of constructed types are List<T>, List<int> and List<List<string>>. A constructed type that uses one or more type parameters, such as List<T>, is called an open constructed type. A constructed type that uses no type parameters, such as List<int>, is called a closed constructed type.
Generic types can be “overloaded” on the number of type parameters; that is two type declarations within the same namespace or outer type declaration can use the same identifier as long as they have a different number of type parameters.
class C {}
class C<V> {}
struct C<U,V> {} // Error, C with two type parameters defined twice
class C<A,B> {} // Error, C with two type parameters defined twice
The type lookup rules used during type name resolution (§20.9.1), simple name resolution (§20.9.5), and member access (§20.9.6) respect the number of type parameters.
The base interfaces of a generic class declaration must satisfy the uniqueness rule described in §20.3.1.
20.1.1 Type parameters
Type parameters can be supplied in a class declaration. Each type parameter is a simple identifier that denotes a placeholder for a type argument supplied to create a constructed type. A type parameter is a formal placeholder for a type that will be supplied later. By constrast, a type argument (§20.5.1) is the actual type that is substituted for the type parameter when a constructed type is created.
type-parameter-list:
< type-parameters >
type-parameters:
attributesopt type-parameter
type-parameters , attributesopt type-parameter
type-parameter:
identifier
Each type parameter in a class declaration defines a name in the declaration space (§3.3) of that class. Thus, it cannot have the same name as another type parameter or a member declared in that class. A type parameter cannot have the same name as the type itself.
The scope (§3.7) of a type parameter on a class includes the class-base, type-parameter-constraints-clauses, and class-body. Unlike members of a class, this scope does not extend to derived classes. Within its scope, a type parameter can be used as a type.
type:
value-type
reference-type
type-parameter
Since a type parameter can be instantiated with many different actual type arguments, type parameters have slightly different operations and restrictions than other types. These include:
· A type parameter cannot be used directly to declare a base class or interface (§20.1.3).
· The rules for member lookup on type parameters depend on the constraints, if any, applied to the type parameter. They are detailed in §20.7.2.
· The available conversions for a type parameter depend on the constraints, if any, applied to the type parameter. They are detailed in §20.7.4.
· The literal null cannot be converted to a type given by a type parameter, except if the type parameter is known to be a reference type (§20.7.4). However, a default value expression (§25.6) can be used instead. In addition, a value with a type given by a type parameter can be compared with null using == and != (§20.8.3) unless the type parameter has the value type constraint (§20.7.4).
· A new expression (§20.8.1) can only be used with a type parameter if the type parameter is constrained by a constructor-constraint or the value type constraint (§20.7).
· A type parameter cannot be used anywhere within an attribute.
· A type parameter cannot be used in a member access or type name to identify a static member or a nested type (§20.9.1, §20.9.6).
· In unsafe code, a type parameter cannot be used as an unmanaged-type (§18.2).
As a type, type parameters are purely a compile-time construct. At run-time, each type parameter is bound to a run-time type that was specified by supplying a type argument to the generic type declaration. Thus, the type of a variable declared with a type parameter will, at run-time, be a closed constructed type (§20.5.2). The run-time execution of all statements and expressions involving type parameters uses the actual type that was supplied as the type argument for that parameter.
20.1.2 The instance type
Each class declaration has an associated constructed type, the instance type. For a generic class declaration, the instance type is formed by creating a constructed type (§20.4) from the type declaration, with each of the supplied type arguments being the corresponding type parameter. Since the instance type uses the type parameters, it can only be used where the type parameters are in scope; that is, inside the class declaration. The instance type is the type of this for code written inside the class declaration. For non-generic classes, the instance type is simply the declared class. The following shows several class declarations along with their instance types:
class A<T> // instance type: A<T>
{
class B {} // instance type: A<T>.B
class C<U> {} // instance type: A<T>.C<U>
}
class D {} // instance type: D
20.1.3 Base specification
The base class specified in a class declaration can be a constructed class type (§20.4). A base class cannot be a type parameter on its own, though it can involve the type parameters that are in scope.
class Extend<V>: V {} // Error, type parameter used as base class
A generic class declaration cannot use System.Attribute as a direct or indirect base class.
The base interfaces specified in a class declaration can be constructed interface types (§20.4). A base interface cannot be a type parameter on its own, though it can involve the type parameters that are in scope. The following code illustrates how a class can implement and extend constructed types:
class C<U,V> {}
interface I1<V> {}
class D: C<string,int>, I1<string> {}
class E<T>: C<int,T>, I1<T> {}
The base interfaces of a generic class declaration must satisfy the uniqueness rule described in §20.3.1.
Methods in a class that override or implement methods from a base class or interface must provide appropriate methods of specialized types. The following code illustrates how methods are overridden and implemented. This is explained further in §20.1.10.
class C<U,V>
{
public virtual void M1(U x, List<V> y) {...}
}
interface I1<V>
{
V M2(V);
}
class D: C<string,int>, I1<string>
{
public override void M1(string x, List<int> y) {...}
public string M2(string x) {...}
}
20.1.4 Members of generic classes
All members of a generic class can use type parameters from any enclosing class, either directly or as part of a constructed type. When a particular closed constructed type (§20.5.2) is used at run-time, each use of a type parameter is replaced with the actual type argument supplied to the constructed type. For example:
class C<V>
{
public V f1;
public C<V> f2 = null;
public C(V x) {
this.f1 = x;
this.f2 = this;
}
}
class Application
{
static void
C<int> x1 = new C<int>(1);
Console.WriteLine(x1.f1); // Prints 1
C<double> x2 = new C<double>(3.1415);
Console.WriteLine(x2.f1); // Prints 3.1415
}
}
Within instance function members, the type of this is the instance type (§20.1.2) of the containing declaration.
Apart from the use of type parameters as types, members in generic class declarations follow the same rules as members of non-generic classes. Additional rules that apply to particular kinds of members are discussed in the following sections.
20.1.5 Static fields in generic classes
A static variable in a generic class declaration is shared amongst all instances of the same closed constructed type (§20.5.2), but is not shared amongst instances of different closed constructed types. These rules apply regardless of whether the type of the static variable involves any type parameters or not.
For example:
class C<V>
{
static int count = 0;
public C() {
count++;
}
public static int Count {
get { return count; }
}
}
class Application
{
static void
C<int> x1 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 1
C<double> x2 = new C<double>();
Console.WriteLine(C<int>.Count); // Prints 1
C<int> x3 = new C<int>();
Console.WriteLine(C<int>.Count); // Prints 2
}
}
20.1.6 Static constructors in generic classes
A static constructor in a generic class is used to initialize static fields and to perform other initialization for each different closed constructed type that is created from that generic class declaration. The type parameters of the generic type declaration are in scope and can be used within the body of the static constructor.
A new closed constructed class type is initialized the first time that either:
· An instance of the closed constructed type is created.
· Any of the static members of the closed constructed type are referenced.
To initialize a new closed constructed class type, first a new set of static fields (§20.1.5) for that particular closed constructed type is created. Each of the static fields is initialized to its default value (§5.2). Next, the static field initializers (§10.4.5.1) are executed for those static fields. Finally, the static constructor is executed.
Because the static constructor is executed exactly once for each closed constructed class type, it is a convenient place to enforce run-time checks on the type parameter that cannot be checked at compile-time via constraints (§20.6.6). For example, the following type uses a static constructor to enforce that the type argument is an enum:
class Gen<T> where T: struct
{
static Gen() {
if (!typeof(T).IsEnum) {
throw new ArgumentException("T must be an enum");
}
}
}
20.1.7 Accessing protected members
Within a generic class declaration, access to inherited protected instance members is permitted through an instance of any class type constructed from the generic class. Specifically, the rules for accessing protected and protected internal instance members specified in §3.5.3 are augmented with the following rule for generics:
· Within a generic class G, access to an inherited protected instance member M using a primary-expression of the form E.M is permitted if the type of E is a class type constructed from G or a class type inherited from a class type constructed from G.
In the example
class C<T>
{
protected T x;
}
class D<T>: C<T>
{
static void F() {
D<T> dt = new D<T>();
D<int> di = new D<int>();
D<string> ds = new D<string>();
dt.x = default(T);
di.x = 123;
ds.x = "test";
}
}
the three assignments to x are permitted because they all take place through instances of class types constructed from the generic type.
20.1.8 Overloading in generic classes
Methods, constructors, indexers, and operators within a generic class declaration can be overloaded. While signatures as declared must be unique, it is possible that substitution of type arguments results in identical signatures. In such cases, the tie-breaking rules of overload resolution will pick the most specific member.
The following examples show overloads that are valid and invalid according to this rule:
interface I1<T> {...}
interface I2<T> {...}
class G1<U>
{
int F1(U u); // Overload resulotion for G<int>.F1
int F1(int i); // will pick non-generic
void F2(I1<U> a); // Valid overload
void F2(I2<U> a);
}
class G2<U,V>
{
void F3(U u, V v); // Valid, but overload resolution for
void F3(V v, U u); // G2<int,int>.F3 will fail
void F4(U u, I1<V> v); // Valid, but overload resolution for
void F4(I1<V> v, U u); // G2<I1<int>,int>.F4 will fail
void F5(U u1, I1<V> v2); // Valid overload
void F5(V v1, U u2);
void F6(ref U u); // valid overload
void F6(out V v);
}
20.1.9 Parameter array methods and type parameters
Type parameters can be used in the type of a parameter array. For example, given the declaration
class C<V>
{
static void F(int x, int y, params V[] args);
}
the following invocations of the expanded form of the method:
C<int>.F(10, 20);
C<object>.F(10, 20, 30, 40);
C<string>.F(10, 20, "hello", "goodbye");
correspond exactly to:
C<int>.F(10, 20, new int[] {});
C<object>.F(10, 20, new object[] {30, 40});
C<string>.F(10, 20, new string[] {"hello", "goodbye"} );
20.1.10 Overriding and generic classes
Function members in generic classes can override function members in base classes, as usual. When determining the overridden base member, the members of the base classes must be determined by substituting type arguments, as described in §20.5.4. Once the members of the base classes are determined, the rules for overriding are the same as for non-generic classes.
The following example demonstrates how the overriding rules work in the presence of generics:
abstract class C<T>
{
public virtual T F() {...}
public virtual C<T> G() {...}
public virtual void H(C<T> x) {...}
}
class D: C<string>
{
public override string F() {...} // Ok
public override C<string> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<string>
}
class E<T,U>: C<U>
{
public override U F() {...} // Ok
public override C<U> G() {...} // Ok
public override void H(C<T> x) {...} // Error, should be C<U>
}
20.1.11 Operators in generic classes
Generic class declarations can define operators, following the same rules as non-generic class declarations. The instance type (§20.1.2) of the class declaration must be used in the declaration of operators in a manner analogous to the normal rules for operators, as follows:
· A unary operator must take a single parameter of the instance type. The unary ++ and -- operators must return the instance type or a type derived from the instance type.
· At least one of the parameters of a binary operator must be of the instance type.
· Either the parameter type or the return type of a conversion operator must be the instance type.
The following shows some examples of valid operator declarations in a generic class:
class X<T>
{
public static X<T> operator ++(X<T> operand) {...}
public static int operator *(X<T> op1, int op2) {...}
public static explicit operator X<T>(T value) {...}
}
For a conversion operator that converts from a source type S to a target type T, when the rules specified in §10.9.3 are applied, any type parameters associated with S or T are considered to be unique types that have no inheritance relationship with other types, and any constraints on those type parameters are ignored.
In the example
class C<T> {...}
class D<T>: C<T>
{
public static implicit operator C<int>(D<T> value) {...} // Ok
public static implicit operator C<string>(D<T> value) {...} // Ok
public static implicit operator C<T>(D<T> value) {...} // Error
}
the first two operator declarations are permitted because, for the purposes of §10.9.3, T and int and string respectively are considered unique types with no relationship. However, the third operator is an error because C<T> is the base class of D<T>.
It is possible to declare operators that, for some type arguments, specify conversions that already exist as pre-defined conversions. In the example
struct Convertible<T>
{
public static implicit operator Convertible<T>(T value) {...}
public static explicit operator T(Convertible<T> value) {...}
}
when type object is specified as a type argument for T, the second operator declares a conversion that already exists (an implicit, and therefore also an explicit, conversion exists from any type to type object).
In cases where a pre-defined conversion exists between two types, any user-defined conversions between those types are ignored. Specifically:
· If a pre-defined implicit conversion (§6.1) exists from type S to type T, all user-defined conversions (implicit or explicit) from S to T are ignored.
· If a pre-defined explicit conversion (§6.2) exists from type S to type T, any user-defined explicit conversions from S to T are ignored. However, user-defined implicit conversions from S to T are still considered.
For all types but object, the operators declared by the Convertible<T> type above do not conflict with pre-defined conversions. For example:
void F(int i, Convertible<int> n) {
i = n; // Error
i = (int)n; // User-defined explicit conversion
n = i; // User-defined implicit conversion
n = (Convertible<int>)i; // User-defined implicit conversion
}
However, for type object, pre-defined conversions hide the user-defined conversions in all cases but one:
void F(object o, Convertible<object> n) {
o = n; // Pre-defined boxing conversion
o = (object)n; // Pre-defined boxing conversion
n = o; // User-defined implicit conversion
n = (Convertible<object>)o; // Pre-defined unboxing conversion
}
20.1.12 Nested types in generic classes
A generic class declaration can contain nested type declarations. The type parameters of the enclosing class can be used within the nested types. A nested type declaration can contain additional type parameters that apply only to the nested type.
Every type declaration contained within a generic class declaration is implicitly a generic type declaration. When writing a reference to a type nested within a generic type, the containing constructed type, including its type arguments, must be named. However, from within the outer class, the nested type can be used without qualification; the instance type of the outer class can be implicitly used when constructing the nested type. The following example shows three different correct ways to refer to a constructed type created from Inner; the first two are equivalent:
class Outer<T>
{
class Inner<U>
{
public static void F(T t, U u) {...}
}
static void F(T t) {
Outer<T>.Inner<string>.F(t, "abc"); // These two statements have
Inner<string>.F(t, "abc"); // the same effect
Outer<int>.Inner<string>.F(3, "abc"); // This type is different
Outer.Inner<string>.F(t, "abc"); // Error, Outer needs type arg
}
}
Although it is bad programming style, a type parameter in a nested type can hide a member or type parameter declared in the outer type:
class Outer<T>
{
class Inner<T> // Valid, hides Outer’s T
{
public T t; // Refers to Inner’s T
}
}
20.1.13 Application entry point
The application entry point method (§3.1) may not be in a generic class declaration.
20.2 Generic struct declarations
A struct declaration can optionally define type parameters and their associated constraints:
struct-declaration:
attributesopt struct-modifiersopt struct identifier type-parameter-listopt struct-interfacesopt
type-parameter-constraints-clausesopt struct-body ;opt
The rules for generic class declarations apply equally to generic struct declarations, as do the exceptions noted in §11.3.
20.3 Generic interface declarations
An interface declaration can optionally define type parameters and their associated constraints:
interface-declaration:
attributesopt interface-modifiersopt interface identifier type-parameter-listopt
interface-baseopt type-parameter-constraints-clausesopt interface-body ;opt
Except where noted in the following, generic interface declarations follow the same rules as non-generic interface declarations.
Each type parameter in an interface declaration defines a name in the declaration space (§3.3) of that interface. The scope (§3.7) of a type parameter on an interface includes the interface-base, type-parameter-constraints-clauses, and interface-body. Within its scope, a type parameter can be used as a type. The same restrictions apply to type parameters on interfaces as apply to type parameters on classes (§20.1.1).
Methods within generic interfaces are subject to the same overload rules as methods within generic classes (§20.1.8).
20.3.1 Uniqueness of implemented interfaces
The interfaces implemented by a generic type declaration must remain unique for all possible constructed types. Without this rule, it would be impossible to determine the correct method to call for certain constructed types. For example, suppose a generic class declaration were permitted to be written as follows:
interface I<T>
{
void F();
}
class X<U,V>: I<U>, I<V> // Error: I<U> and I<V> conflict
{
void I<U>.F() {...}
void I<V>.F() {...}
}
Were this permitted, it would be impossible to determine which code to execute in the following case:
I<int> x = new X<int,int>();
x.F();
To determine if the interface list of a generic type declaration is valid, the following steps are performed:
· Let L be the list of interfaces directly specified in a generic class, struct, or interface declaration C.
· Add to L any base interfaces of the interfaces already in L.
· Remove any duplicates from L.
· If any possible constructed type created from C would, after type arguments are substituted into L, cause two interfaces in L to be identical, then the declaration of C is invalid. Constraint declarations are not considered when determining all possible constructed types.
In the class declaration X above, the interface list L consists of I<U> and I<V>. The declaration is invalid because any constructed type with U and V being the same type would cause these two interfaces to be identical types.
It is possible for interfaces specified at different inheritance levels to unify:
interface I<T>
{
void F();
}
class Base<U>: I<U>
{
void I<U>.F() {…}
}
class Derived<U,V>: Base<U>, I<V> // Ok
{
void I<V>.F() {…}
}
This code is valid even though Derived<U,V> implements both I<U> and I<V>. The code
I<int> x = new X<int,int>();
x.F();
invokes the method in Derived, since Derived<int,int> effectively re-implements I<int> (§13.4.4).
20.3.2 Explicit interface member implementations
Explicit interface member implementations work with constructed interface types in essentially the same way as with simple interface types. As usual, an explicit interface member implementation must be qualified by an interface-type indicating which interface is being implemented. This type can be a simple interface or a constructed interface, as in the following example:
interface IList<T>
{
T[] GetElements();
}
interface IDictionary<K,V>
{
V this[K key];
void Add(K key, V value);
}
class List<T>: IList<T>, IDictionary<int,T>
{
T[] IList<T>.GetElements() {...}
T IDictionary<int,T>.this[int index] {...}
void IDictionary<int,T>.Add(int index, T value) {...}
}
20.4 Generic delegate declarations
A delegate declaration can optionally define type parameters and their associated constraints:
delegate-declaration:
attributesopt delegate-modifiersopt delegate return-type identifier type-parameter-listopt
( formal-parameter-listopt ) type-parameter-constraints-clausesopt ;
Generic delegate declarations follow the same rules as non-generic delegate declarations, except where noted in the following. Each type parameter in a generic delegate declaration defines a name in a special declaration space (§3.3) that is associated with that delegate declaration. The scope (§3.7) of a type parameter in a delegate declaration includes the return-type, formal-parameter-list, and type-parameter-constraints-clauses.
Like other generic type declarations, type arguments must be given to create a constructed delegate type. The parameter types and return type of a constructed delegate type are created by substituting, for each type parameter in the delegate declaration, the corresponding type argument of the constructed delegate type. The resulting return type and parameter types are used in determining what methods are compatible with a constructed delegate type. For example:
delegate bool Predicate<T>(T value);
class X
{
static bool F(int i) {...}
static bool G(string s) {...}
static void
Predicate<int> p1 = F;
Predicate<string> p2 = G;
}
}
Note that the two assignments in the Main method above are equivalent to the following longer form:
static void
Predicate<int> p1 = new Predicate<int>(F);
Predicate<string> p2 = new Predicate<string>(G);
}
The shorter form is permitted because of method group conversions, which are described in §21.9.
20.5 Constructed types
A generic type declaration, by itself, denotes an unbound generic type that is used as a “blueprint” to form many different types, by way of applying type arguments. The type arguments are written within angle brackets (< and >) immediately following the name of the generic type declaration. A type that is named with at least one type argument is called a constructed type. A constructed type can be used in most places in the language in which a type name can appear. An unbound generic type can only be used within a typeof-expression (§20.8.2).
type-name:
namespace-or-type-name
namespace-or-type-name:
identifier type-argument-listopt
namespace-or-type-name . identifier type-argument-listopt
Constructed types can also be used in expressions as simple names (§20.9.5) or when accessing a member (§20.9.6).
When a namespace-or-type-name is evaluated, only generic types with the correct number of type parameters are considered. Thus, it is possible to use the same identifier to identify different types, as long as the types have different numbers of type parameters. This is useful when mixing generic and non-generic classes in the same program:
namespace Widgets
{
class Queue {...}
class Queue<ElementType> {...}
}
namespace MyApplication
{
using Widgets;
class X
{
Queue q1; // Non-generic Widgets.Queue
Queue<int> q2; // Generic Widgets.Queue
}
}
The detailed rules for name lookup in the namespace-or-type-name productions is described in §20.9. The resolution of ambiguities in these production is described in §20.6.5.
A type-name might identify a constructed type even though it doesn’t specify type parameters directly. This can occur where a type is nested within a generic class declaration, and the instance type of the containing declaration is implicitly used for name lookup (§20.1.12):
class Outer<T>
{
public class Inner {...}
public Inner i; // Type of i is Outer<T>.Inner
}
In unsafe code, a constructed type cannot be used as an unmanaged-type (§18.2).
20.5.1 Type arguments
Each argument in a type argument list is simply a type.
type-argument-list:
< type-arguments >
type-arguments:
type-argument
type-arguments , type-argument
Type arguments can be constructed types or type parameters. In unsafe code (§18), a type-argument may not be a pointer type. Each type argument must satisfy any constraints on the corresponding type parameter (§20.7.1).
20.5.2 Open and closed types
All types can be classified as either open types or closed types. An open type is a type that involves type parameters. More specifically:
· A type parameter defines an open type.
· An array type is an open type if and only if its element type is an open type.
· A constructed type is an open type if and only if one or more of its type arguments is an open type. A constructed nested type is an open type if and only if one or more of its type arguments or the type arguments of its containing type(s) is an open type.
A closed type is a type that is not an open type.
At run-time, all of the code within a generic type declaration is executed in the context of a closed constructed type that was created by applying type arguments to the generic declaration. Each type parameter within the generic type is bound to a particular run-time type. The run-time processing of all statements and expressions always occurs with closed types, and open types occur only during compile-time processing.
Each closed constructed type has its own set of static variables, which are not shared with any other closed constructed types. Since an open type does not exist at run-time, there are no static variables associated with an open type. Two closed constructed types are the same type if they are constructed from the same unbound generic type, and their corresponding type arguments are the same type.
20.5.3 Base classes and interfaces of a constructed type
A constructed class type has a direct base class, just like a simple class type. If the generic class declaration does not specify a base class, the base class is object. If a base class is specified in the generic class declaration, the base class of the constructed type is obtained by substituting, for each type-parameter in the base class declaration, the corresponding type-argument of the constructed type. Given the generic class declarations
class B<U,V> {...}
class G<T>: B<string,T[]> {...}
the base class of the constructed type G<int> would be B<string,int[]>.
Similarly, constructed class, struct, and interface types have a set of explicit base interfaces. The explicit base interfaces are formed by taking the explicit base interface declarations on the generic type declaration, and substituting, for each type-parameter in the base interface declaration, the corresponding type-argument of the constructed type.
The set of all base classes and base interfaces for a type is formed, as usual, by recursively getting the base classes and interfaces of the immediate base classes and interfaces. For example, given the generic class declarations:
class A {...}
class B<T>: A {...}
class C<T>: B<IComparable<T>> {...}
class D<T>: C<T[]> {...}
the base classes of D<int> are C<int[]>, B<IComparable<int[]>>, A, and object.
20.5.4 Members of a constructed type
The non-inherited members of a constructed type are obtained by substituting, for each type-parameter in the member declaration, the corresponding type-argument of the constructed type. The substitution process is based on the semantic meaning of type declarations, and is not simply textual substitution.
For example, given the generic class declaration
class Gen<T,U>
{
public T[,] a;
public void G(int i, T t, Gen<U,T> gt) {...}
public U Prop { get {...} set {...} }
public int H(double d) {...}
}
the constructed type Gen<int[],IComparable<string>> has the following members:
public int[,][] a;
public void G(int i, int[] t, Gen<IComparable<string>,int[]> gt) {...}
public IComparable<string> Prop { get {...} set {...} }
public int H(double d) {...}
The type of the member a in the generic class declaration Gen is “two-dimensional array of T”, so the type of the member a in the constructed type above is “two-dimensional array of one-dimensional array of int”, or int[,][].
The inherited members of a constructed type are obtained in a similar way. First, all the members of the immediate base class are determined. If the base class is itself a constructed type, this might involve a recursive application of the current rule. Then, each of the inherited members is transformed by substituting, for each type-parameter in the member declaration, the corresponding type-argument of the constructed type.
class B<U>
{
public U F(long index) {...}
}
class D<T>: B<T[]>
{
public T G(string s) {...}
}
In the above example, the constructed type D<int> has a non-inherited member public int G(string s) obtained by substituting the type argument int for the type parameter T. D<int> also has an inherited member from the class declaration B. This inherited member is determined by first determining the members of the constructed type B<T[]> by substituting T[] for U, yielding public T[] F(long index). Then, the type argument int is substituted for the type parameter T, yielding the inherited member public int[] F(long index).
20.5.5 Accessibility of a constructed type
A constructed type C<T1, ...,TN> is accessible when all of its components C, T1, ..., TN are accessible. More precisely, the accessibility domain for a constructed type is the intersection of the accessibility domain of the unbound generic type and the accessibility domains of the type arguments.
20.5.6 Conversions
Constructed types follow the same conversion rules (§6) as do non-generic types. When applying these rules, the base classes and interfaces of constructed types must be determined as described in §20.5.3.
No special conversions exist between constructed reference types other than those described in §6. In particular, unlike array types, constructed reference types do not exhibit “covariant” conversions. This means that a type List<B> has no conversion (either implicit or explicit) to List<A> even if B is derived from A. Likewise, no conversion exists from List<B> to List<object>.
The rationale for this is simple: if a conversion to List<A> is permitted, then apparently one can store values of type A into the list. But this would break the invariant that every object in a list of type List<B> is always a value of type B, or else unexpected failures may occur when assigning into collection classes.
The behavior of conversions and runtime type checks is illustrated below:
class A {...}
class B: A {...}
class Collection {...}
class List<T>: Collection {...}
class Test
{
void F() {
List<A> listA = new List<A>();
List<B> listB = new List<B>();
Collection c1 = listA; // Ok, List<A> is a Collection
Collection c2 = listB; // Ok, List<B> is a Collection
List<A> a1 = listB; // Error, no implicit conversion
List<A> a2 = (List<A>)listB; // Error, no explicit conversion
}
}
20.5.7 Using alias directives
Using aliases can name a closed constructed type, but cannot name a generic type declaration without supplying type arguments. For example:
namespace N1
{
class A<T>
{
class B {}
}
}
namespace N2
{
using W = N1.A; // Error, cannot name generic type
using X = N1.A.B; // Error, cannot name generic type
using Y = N1.A<int>; // Ok, can name closed constructed type
using Z<T> = N1.A<T>; // Error, using alias cannot have type parameters
}
20.5.8 Attributes
A typeof-expression (§20.8.2) used as an attribute argument expression can reference a non-generic type, a closed constructed type, or an unbound generic type, but it cannot reference an open type.
class A: Attribute
{
public A(Type t) {...}
}
class G<T>
{
[A(typeof(T))] T t; // Error, open type in attribute
}
class X
{
[A(typeof(List<int>))] int x; // Ok, closed constructed type
[A(typeof(List<>))] int y; // Ok, unbound generic type
}
20.5.9 Arrays and the generic ILi st interface
A one-dimensional array T[] implements the interface System.Collections.Generic.IList<T> (IList<T> for short) and its base interfaces. Accordingly, there is an implicit conversion from T[] to IList<T> and its base interfaces. In addition, if there is an implicit reference conversion from S to T then S[] implements IList<T> and there is an implicit reference conversion from S[] to IList<T> and its base interfaces (§6.1.4). If there is an explicit reference conversion from S to T then there is an explicit reference conversion from S[] to IList<T> and its base interfaces (§6.2.3). For example:
using System.Collections.Generic;
class Test
{
static void Main() {
string[] sa = new string[5];
object[] oa1 = new object[5];
object[] oa2 = sa;
IList<string> lst1 = sa; // Ok
IList<string> lst2 = oa1; // Error, cast needed
IList<object> lst3 = sa; // Ok
IList<object> lst4 = oa1; // Ok
IList<string> lst5 = (IList<string>)oa1; // Exception
IList<string> lst6 = (IList<string>)oa2; // Ok
}
}
The assignment lst2 = oa1 generates a compile-time error since the conversion from object[] to IList<string> is an explicit conversion, not implicit. The cast (IList<string>)oa1 will cause an exception to be thrown at runtime since oa1 references an object[] and not a string[]. However the cast (IList<string>)oa2 will not cause an exception to be thrown since oa2 references a string[].
Whenever there is an implicit or explicit reference conversion from S[] to IList<T>, there is also an explicit reference conversion from IList<T> and its base interfaces to S[] (§6.2.3).
When an array type S[] implements IList<T>, some of the members of the implemented interface may throw exceptions. The precise behavior of the implementation of the interface is beyond the scope of this specification.
20.6 Generic methods
A generic method is a method whose declaration includes a type parameter. Generic methods may be declared inside class, struct, or interface declarations, which may themselves be either generic or non-generic. If a generic method is declared inside a generic type declaration, the body of the method can refer to both the type parameters of the method and the type parameters of the containing declaration.
class-member-declaration:
…
generic-method-declaration
struct-member-declaration:
…
generic-method-declaration
interface-member-declaration:
…
interface-generic-method-declaration
Generic methods are declared by placing a type parameter list following the name of the method:
generic-method-declaration:
generic-method-header method-body
generic-method-header:
attributesopt method-modifiersopt return-type member-name type-parameter-list
( formal-parameter-listopt ) type-parameter-constraints-clausesopt
interface-generic-method-declaration:
attributesopt newopt return-type identifier type-parameter-list
( formal-parameter-listopt ) type-parameter-constraints-clausesopt ;
The type-parameter-list and type-parameter-constraints-clauses of a generic method declaration have the same syntax and purpose as in a generic type declaration. The method’s type-parameters are in scope throughout the method-declaration, and can be used to form types throughout that scope in return-type, method-body, and type-parameter-constraints-clauses but not in attributes.
The name of a method type parameter cannot be the same as the name of an ordinary parameter in the same method.
The following example finds the first element in an array, if any, that satisfies the given test delegate. (Generic delegates are described in §20.4.)
public delegate bool Test<T>(T item);
public class Finder
{
public static T Find<T>(T[] items, Test<T> test) {
foreach (T item in items) {
if (test(item)) return item;
}
throw new InvalidOperationException("Item not found");
}
}
A generic method may not be declared extern. All other method modifiers are valid on a generic method.
20.6.1 Generic method signatures
For the purposes of signature comparisons any type parameter constraints clauses are ignored, as are the names of the method’s type parameters, but the number of generic type parameters is relevant, as are the ordinal positions of type parameters in left-to-right ordering. The following example shows how method signatures are affected by this rule:
class A {}
class B {}
interface IX
{
T F1<T>(T[] a, int i); // Error, both declarations have the same
void F1<U>(U[] a, int i); // signature because return type and type
// parameter names are not significant
void F2<T>(int x); // Ok, the number of type parameters is part
void F2(int x); // of the signature
void F3<T>(T t) where T: A; // Error, constraints are not
void F3<T>(T t) where T: B; // considered in signatures
}
20.6.2 Virtual generic methods
Generic methods can be declared using the abstract, virtual, and override modifiers. The signature matching rules described in §20.6.1 are used when matching methods for overriding or interface implementation. When a generic method overrides a generic method declared in a base class, or is an explicit interface member implementation of a method in a base interface, the method cannot specify any type-parameter-constraints-clauses. In these cases, the type parameters of the method inherit constraints from the method being overridden or implemented. For example:
abstract class Base
{
public abstract T F<T,U>(T t, U u) where U: T;
public abstract T G<T>(T t) where T: IComparable;
}
interface I
{
bool M<T>(T a, T, b) where T: class;
}
class Derived: Base, I
{
// A "Y: X" constraint is implicitly inherited,
// so y is implicitly convertible to type X
public override X F<X,Y>(X x, Y y) {
return y;
}
// This method is in error because an override
// cannot include a where clause
public override T G<T>(T t) where T: IComparable {
...
}
// A "U: class" constraint is implicitly inherited,
// so a and b can be compared using the reference
// type equality operators
bool I.M<U>(U a, U b)
{
return a == b;
}
}
The override of F is valid because type parameter names are permitted to differ. Within Derived.F, the type parameter Y implicitly has the constraint Y: X as inherited from Base.F. The override of G is invalid because overrides are not permitted to specify type parameter constraints. The explicit method implementation I.M in Derived implicitly inherits the U: class constraint from the interface method.
When a generic method implicitly implements an interface method, the constraints given for each method type parameter must be equivalent in both declarations (after any interface type parameters are replaced with the appropriate type arguments), where method type parameters are identified by ordinal positions, left to right. For example:
interface I<A,B,C>
{
void F<T>(T t) where T: A;
void G<T>(T t) where T: B;
void H<T>(T t) where T: C;
}
class C: I<object,C,string>
{
public void F<T>(T t) {...} // Ok
public void G<T>(T t) where T: C {...} // Ok
public void H<T>(T t) where T: string {...} // Error
}
The method C.F<T> implicitly implements I<object,C,string>.F<T>. In this case, C.F<T> is not required (nor permitted) to specify the constraint T: object since object is an implicit constraint on all type parameters. The method C.G<T> implicitly implements I<object,C,string>.G<T> because the constraints match those in the interface, after the interface type parameters are replaced with the corresponding type arguments. The constraint for method C.H<T> is an error because sealed types (string in this case) cannot be used as constraints. Omitting the constraint would also be an error since constraints of implicit interface method implementations are required to match. Thus, it is impossible to implicitly implement I<object,C,string>.H<T>. This interface method can only be implemented using an explicit interface member implementation:
class C: I<object,C,string>
{
...
public void H<U>(U u) where U: class {...}
void I<object,C,string>.H<T>(T t) {
string s = t; // Ok
H<T>(t);
}
}
In this example, the explicit interface member implementation invokes a public method having strictly weaker constraints. Note that the assignment from t to s is valid since T inherits a constraint of T: string, even though this constraint is not expressible in source code.
20.6.3 Calling generic methods
A generic method invocation can explicitly specify a type argument list, or it can omit the type argument list and rely on type inference to determine the type arguments. The exact compile-time processing of a method invocation, including a generic method invocation, is described in §20.9.7. When a generic method is invoked without a type argument list, type inference takes place as described in §20.6.4.
The following example shows how overload resolution occurs after type inference and after type arguments are substituted into the parameter list:
class Test
{
static void F<T>(int x, T y) {
Console.WriteLine("one");
}
static void F<T>(T x, long y) {
Console.WriteLine("two");
}
static void
F<int>(5, 324); // Ok, prints "one"
F<byte>(5, 324); // Ok, prints "two"
F<double>(5, 324); // Error, ambiguous
F(5, 324); // Ok, prints "one"
F(5, 324L); // Error, ambiguous
}
}
20.6.4 Inference of type arguments
When a generic method is called without specifying type arguments, a type inference process attempts to infer type arguments for the call. The presence of type inference allows a more convenient syntax to be used for calling a generic method, and allows the programmer to avoid specifying redundant type information. For example, given the method declaration:
class Chooser
{
static Random rand = new Random();
public static T Choose<T>(T first, T second) {
return (rand.Next(2) == 0)? first: second;
}
}
it is possible to invoke the Choose method without explicitly specifying a type argument:
int i = Chooser.Choose(5, 213); // Calls Choose<int>
string s = Chooser.Choose("foo", "bar"); // Calls Choose<string>
Through type inference, the type arguments int and string are determined from the arguments to the method.
Type inference occurs as part of the compile-time processing of a method invocation (§20.9.7) and takes place before the overload resolution step of the invocation. When a particular method group is specified in a method invocation, and no type arguments are specified as part of the method invocation, type inference is applied to each generic method in the method group. If type inference succeeds, then the inferred type arguments are used to determine the types of arguments for subsequent overload resolution. If overload resolution chooses a generic method as the one to invoke, then the inferred type arguments are used as the actual type arguments for the invocation. If type inference for a particular method fails, that method does not participate in overload resolution. The failure of type inference, in and of itself, does not cause a compile-time error. However, it often leads to a compile-time error when overload resolution then fails to find any applicable methods.
If the supplied number of arguments is different than the number of parameters in the method, then inference immediately fails. Otherwise, type inference first occurs independently for each regular argument that is supplied to the method. Assume this argument has type A, and the corresponding parameter has type P. Type inferences are produced by relating the types A and P according to the following steps:
· Nothing is inferred from the argument (but type inference succeeds) if any of the following are true:
o P does not involve any method type parameters.
o The argument is the null literal.
o The argument is an anonymous method.
o The argument is a method group.
· The following steps are repeated as long as one is true:
o If P is an array type and A is an array type with the same rank, then replace A and P, respectively, with the element types of A and P.
o If P is a type constructed from IEnumerable<T>, ICollection<T>, or IList<T> (all in the System.Collections.Generic namespace) and A is a single-dimensional array type, then replace A and P, respectively, with the element types of A and P.
· If P is an array type (meaning that the previous step failed to relate A and P), then type inference fails for the generic method.
· If P is a method type parameter, then type inference succeeds for this argument, and A is the type inferred for that type parameter.
· Otherwise, P must be a constructed type. If, for each method type parameter MX that occurs in P, exactly one type TX can be determined such that replacing each MX with each TX produces a type to which A is convertible by a standard implicit conversion, then inferencing succeeds for this argument, and each TX is the type inferred for each MX. Method type parameter constraints, if any, are ignored for the purpose of type inference. If, for a given MX, no TX exists or more than one TX exists, then type inference fails for the generic method (a situation where more than one TX exists can only occur if P is a generic interface type and A implements multiple constructed versions of that interface).
If all of the method arguments are processed successfully by the above algorithm, then all inferences that were produced from the arguments are pooled. Type inference is said to have succeeded for the given generic method and argument list if both of the following are true:
· Each type parameter of the method had a type argument inferred for it (in short, the set of inferences is complete).
· For each type parameter, all of the inferences for that type parameter infer the same type argument (in short, the set of inferences is consistent).
If the generic method was declared with a parameter array (§10.5.1.4), then type inference is first performed against the method in its normal form. If type inference succeeds, and the resultant method is applicable, then the method is eligible for overload resolution in its normal form. Otherwise, type inference is performed against the method in its expanded form (§7.4.2.1).
20.6.5 Grammar ambiguities
The productions for simple-name (§20.9.5) and member-access (§20.9.6) can give rise to ambiguities in the grammar for expressions. For example, the statement:
F(G<A,B>(7));
could be interpreted as a call to F with two arguments, G < A and B > (7). Alternatively, it could be interpreted as a call to F with one argument, which is a call to a generic method G with two type arguments and one regular argument.
If a sequence of tokens can be parsed (in context) as a simple-name (§20.9.5), member-access (§20.9.6), or pointer-member-access (§18.5.2) ending with a type-argument-list (§20.5.1), the token immediately following the closing > token is examined. If it is one of
( ) ] : ; , . ? == !=
then the type-argument-list is retained as part of the simple-name, member-access or pointer-member-access and any other possible parse of the sequence of tokens is discarded. Otherwise, the type-argument-list is not considered to be part of the simple-name, member-access or pointer-member-access, even if there is no other possible parse of the sequence of tokens. Note that these rules are not applied when parsing a type-argument-list in a namespace-or-type-name (§20.9.1). The statement
F(G<A,B>(7));
will, according to this rule, be interpreted as a call to F with one argument, which is a call to a generic method G with two type arguments and one regular argument. The statements
F(G < A, B > 7);
F(G < A, B >> 7);
will each be interpreted as a call to F with two arguments. The statement
x = F < A > +y;
will be interpreted as a less than operator, greater than operator, and unary plus operator, as if the statement had been written x = (F < A) > (+y), instead of as a simple-name with a type-argument-list followed by a binary plus operator. In the statement
x = y is C<T> + z;
the tokens C<T> are interpreted as a namespace-or-type-name with a type-argument-list.
20.6.6 Using a generic method with a delegate
An instance of a delegate can be created that refers to a generic method declaration. The exact compile-time processing of a delegate creation expression, including a delegate creation expression that refers to a generic method, is described in §21.10.
The type arguments used when invoking a generic method through a delegate are determined when the delegate is instantiated. The type arguments can be given explicitly via a type-argument-list, or determined by type inference (§20.6.4). If type inference is used, the parameter types of the delegate are used as argument types in the inference process. The return type of the delegate is not used for inference. The following example shows both ways of supplying a type argument to a delegate instantiation expression:
delegate int D(string s, int i);
delegate int E();
class X
{
public static T F<T>(string s, T t) {...}
public static T G<T>() {...}
static void
D d1 = new D(F<int>); // Ok, type argument given explicitly
D d2 = new D(F); // Ok, int inferred as type argument
E e1 = new E(G<int>); // Ok, type argument given explicitly
E e2 = new E(G); // Error, cannot infer from return type
}
}
Whenever a generic method is used to create delegate instance, type arguments are given or inferred when the delegate instance is created, and a type-argument-list cannot be supplied when the delegate is invoked.
20.6.7 Members that cannot be generic
Properties, events, indexers, operators, constructors, and destructors cannot themselves have type parameters. However, they can occur in generic types and use the type parameters from an enclosing type.
20.7 Constraints
Generic type and method declarations can optionally specify type parameter constraints by including type-parameter-constraints-clauses.
type-parameter-constraints-clauses:
type-parameter-constraints-clause
type-parameter-constraints-clauses type-parameter-constraints-clause
type-parameter-constraints-clause:
where type-parameter : type-parameter-constraints
type-parameter-constraints:
primary-constraint
secondary-constraints
constructor-constraint
primary-constraint , secondary-constraints
primary-constraint , constructor-constraint
secondary-constraints , constructor-constraint
primary-constraint , secondary-constraints , constructor-constraint
primary-constraint:
class-type
class
struct
secondary-constraints:
interface-type
type-parameter
secondary-constraints , interface-type
secondary-constraints , type-parameter
constructor-constraint:
new ( )
Each type-parameter-constraints-clause consists of the token where, followed by the name of a type parameter, followed by a colon and the list of constraints for that type parameter. There can be at most one where clause for each type parameter, and the where clauses can be listed in any order. Like the get and set tokens in a property accessor, the where token is not a keyword.
The list of constraints given in a where clause can include any of the following components, in this order: a single primary constraint, one or more secondary constraints, and the constructor constraint, new().
A primary constraint can be a class type or the reference type constraint class or the value type constraint struct. A secondary constraint can be a type-parameter or interface-type.
The reference type constraint specifies that a type argument used for the type parameter must be a reference type. All class types, interface types, delegate types, array types, and type parameters known to be a reference type (as defined below) satisfy this constraint.
The value type constraint specifies that a type argument used for the type parameter must be a non-nullable value type. All non-nullable struct types, enum types, and type parameters having the value type constraint satisfy this constraint. Note that although classified as a value type, a nullable type (§24.1) does not satisfy the value type constraint. A type parameter having the value type constraint cannot also have the constructor-constraint.
Pointer types are never allowed to be type arguments and are not considered to satisfy either the reference type or value type constraints.
If a constraint is a class type, an interface type, or a type parameter, that type specifies a minimal “base type” that every type argument used for that type parameter must support. Whenever a constructed type or generic method is used, the type argument is checked against the constraints on the type parameter at compile-time. The type argument supplied must derive from or implement all of the constraints given for that type parameter.
A class-type constraint must satisfy the following rules:
· The type must be a class type.
· The type must not be sealed.
· The type must not be one of the following types: System.Array, System.Delegate, System.Enum, or System.ValueType.
· The type must not be object. Because all types derive from object, such a constraint would have no effect if it were permitted.
· At most one constraint for a given type parameter can be a class type.
A type specified as an interface-type constraint must satisfy the following rules:
· The type must be an interface type.
· A type must not be specified more than once in a given where clause.
In either case, the constraint can involve any of the type parameters of the associated type or method declaration as part of a constructed type, and can involve the type being declared.
Any class or interface type specified as a type parameter constraint must be at least as accessible (§3.5.4) as the generic type or method being declared.
A type specified as a type-parameter constraint must satisfy the following rules:
· The type must be a type parameter.
· A type must not be specified more than once in a given where clause.
In addition there must be no cycles in the dependency graph of type parameters, where dependency is a transitive relation defined by:
· If a type parameter T is used as a constraint for type parameter S then S depends on T.
· If a type parameter S depends on a type parameter T and T depends on a type parameter U then S depends on U.
Given this relation, it is a compile-time error for a type parameter to depend on itself (directly or indirectly).
Any constraints must be consistent among dependent type parameters. If type parameter S depends on type parameter T then:
· T must not have the value type constraint. Otherwise, T is effectively sealed so S would be forced to be the same type as T, eliminating the need for two type parameters.
· If S has the value type constraint then T must not have a class-type constraint.
· If S has a class-type constraint A and T has a class-type constraint B then there must be an identity conversion or implicit reference conversion from A to B or an implicit reference conversion from B to A.
· If S also depends on type parameter U and U has a class-type constraint A and T has a class-type constraint B then there must be an identity conversion or implicit reference conversion from A to B or an implicit reference conversion from B to A.
It is valid for S to have the value type constraint and T to have the reference type constraint. Effectively this limits T to the types System.Object, System.ValueType, System.Enum, and any interface type.
If the where clause for a type parameter includes a constructor constraint (which has the form new()), it is possible to use the new operator to create instances of the type (§20.8.1). Any type argument used for a type parameter with a constructor constraint must have a public parameterless constructor (this constructor implicitly exists for any value type) or be a type parameter having the value type constraint or constructor constraint (see §20.7.1 for details).
The following are examples of constraints:
interface IPrintable
{
void Print();
}
interface IComparable<T>
{
int CompareTo(T value);
}
interface IKeyProvider<T>
{
T GetKey();
}
class Printer<T> where T: IPrintable {...}
class SortedList<T> where T: IComparable<T> {...}
class Dictionary<K,V>
where K: IComparable<K>
where V: IPrintable, IKeyProvider<K>, new()
{
...
}
The following example is in error because it causes a circularity in the dependency graph of the type parameters:
class Circular<S,T>
where S: T
where T: S // Error, circularity in dependency graph
{
...
}
The following examples illustrate additional invalid situations:
class Sealed<S,T>
where S: T
where T: struct // Error, T is sealed
{
...
}
class A {...}
class B {...}
class Incompat<S,T>
where S: A, T
where T: B // Error, incompatible class-type constraints
{
...
}
class StructWithClass<S,T,U>
where S: struct, T
where T: U
where U: A // Error, A incompatible with struct
{
...
}
The effective base class of a type parameter T is defined as follows:
· If T has no primary constraints or type parameter constraints, its effective base class is object.
· If T has the value type constraint, its effective base class is System.ValueType.
· If T has a class-type constraint C but no type-parameter constraints, its effective base class is C.
· If T has no class-type constraint but has one or more type-parameter constraints, its effective base class is the most encompassed type (§6.4.2) in the set of effective base classes of its type-parameter constraints. The consistency rules ensure that such a most encompassed type exists.
· If T has both a class-type constraint and one or more type-parameter constraints, its effective base class is the most encompassed type (§6.4.2) in the set consisting of the class-type constraint of T and the effective base classes of its type-parameter constraints. The consistency rules ensure that such a most encompassed type exists.
· If T has the reference type constraint but no class-type constraints, its effective base class is object.
The effective interface set of a type parameter T is defined as follows:
· If T has no secondary-constraints, its effective interface set is empty.
· If T has interface-type constraints but no type-parameter constraints, its effective interface set is its set of interface-type constraints.
· If T has no interface-type constraints but has type-parameter constraints, its effective interface set is the union of the effective interface sets of its type-parameter constraints.
· If T has both interface-type constraints and type-parameter constraints, its effective interface set is the union of its set of interface-type constraints and the effective interface sets of its type-parameter constraints.
A type parameter is known to be a reference type if it has the reference type constraint or its effective base class is not object or System.ValueType.
Values of a constrained type parameter type can be used to access the instance members implied by the constraints. In the example
interface IPrintable
{
void Print();
}
class Printer<T> where T: IPrintable
{
void PrintOne(T x) {
x.Print();
}
}
the methods of IPrintable can be invoked directly on x because T is constrained to always implement IPrintable.
20.7.1 Satisfying constraints
Whenever a constructed type or generic method is referenced, the supplied type arguments are checked against the type parameter constraints declared on the generic type or method. For each where clause, the type argument A that corresponds to the named type parameter is checked against each constraint as follows:
· If the constraint is a class type, an interface type, or a type parameter, let C represent that constraint with the supplied type arguments substituted for any type parameters that appear in the constraint. To satisfy the constraint, it must be the case that type A is convertible to type C by one of the following:
o An identity conversion (§6.1.1)
o An implicit reference conversion (§6.1.4)
o A boxing conversion (§6.1.5), provided that type A is a non-nullable value type.
o An implicit reference, boxing or type parameter conversion from a type parameter A to C (§20.7.4).
· If the constraint is the reference type constraint (class), the type A must satisfy one of the following:
o A is an interface type, class type, delegate type or array type. Note that System.ValueType and System.Enum are reference types that satisfy this constraint.
o A is a type parameter that is known to be a reference type (§20.7).
· If the constraint is the value type constraint (struct), the type A must satisfy one of the following:
o A is a struct type or enum type, but not a nullable type. Note that System.ValueType and System.Enum are reference types that do not satisfy this constraint.
o A is a type parameter having the value type constraint (§20.7).
· If the constraint is the constructor constraint new(), the type A must not be abstract and must have a public parameterless constructor. This is satisfied if one of the following is true:
o A is a value type, since all value types have a public default constructor (§4.1.2).
o A is a type parameter having the constructor constraint (§20.7).
o A is a type parameter having the value type constraint (§20.7).
o A is a class that is not abstract and contains an explicitly declared public constructor with no parameters.
o A is not abstract and has a default constructor (§10.10.4).
A compile-time error occurs if one or more of a type parameter’s constraints are not satisfied by the given type arguments.
Since type parameters are not inherited, constraints are never inherited either. In the example below, D needs to specify the constraint on its type parameter T so that T satisfies the constraint imposed by the base class B<T>. In contrast, class E need not specify a constraint, because List<T> implements IEnumerable for any T.
class B<T> where T: IEnumerable {...}
class D<T>: B<T> where T: IEnumerable {...}
class E<T>: B<List<T>> {...}
20.7.2 Member lookup on type parameters
The results of member lookup in a type given by a type parameter T depends on the constraints, if any, specified for T. If T has no class-type, interface-type or type-parameter constraints, then member lookup on T returns the same set of members as member lookup on object. Otherwise, the first stage of member lookup (§20.9.2) considers all the members in the effective base class of T and all the members in each interface in the effective interface set of T. After performing the first stage of member lookup for each of these types, the results are combined, and then hidden members are removed from the combined results.
Before the advent of generics, member lookup always returned either a set of members declared solely in classes, or a set of members declared solely in interfaces and possibly the type object. Member lookup on type parameters changes this somewhat. When a type parameter has both an effective base class other than object and a non-empty effective interface set, member lookup can return a set of members, some of which were declared in a class, and others of which were declared in an interface. The following additional rules handle this case.
· As specified in §20.9.2, during member lookup, members declared in a class other than object hide members declared in interfaces.
· During overload resolution of methods (§7.5.5.1) and indexers (§7.5.6.2), if any applicable member was declared in a class other than object, all members declared in an interface are removed from the set of considered members.
These rules only have effect when doing binding on a type parameter with both an effective base class other than object and a non-empty effective interface set. Informally, members defined in a class type constraint are preferred over members in an interface constraint.
20.7.3 Type parameters and boxing
When a struct type overrides a virtual method inherited from System.Object (such as Equals, GetHashCode, or ToString), invocation of the virtual method through an instance of the struct type doesn’t cause boxing to occur. This is true even when the struct is used as a type parameter and the invocation occurs through an instance of the type parameter type. For example:
using System;
struct Counter
{
int value;
public override string ToString() {
value++;
return value.ToString();
}
}
class Program
{
static void Test<T>() where T: new() {
T x = new T();
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
Console.WriteLine(x.ToString());
}
static void
Test<Counter>();
}
}
The output of the program is:
1
2
3
Although it is bad style for ToString to have side effects, the example demonstrates that no boxing occurred for the three invocations of x.ToString().
Similarly, boxing never implicitly occurs when accessing a member on a constrained type parameter. For example, suppose an interface ICounter contains a method Increment which can be used to modify a value. If ICounter is used as a constraint, the implementation of the Increment method is called with a reference to the variable that Increment was called on, never a boxed copy.
using System;
interface ICounter
{
void Increment();
}
struct Counter: ICounter
{
int value;
public override string ToString() {
return value.ToString();
}
void ICounter.Increment() {
value++;
}
}
class Program
{
static void Test<T>() where T: ICounter, new() {
T x = new T();
Console.WriteLine(x);
x.Increment(); // Modify x
Console.WriteLine(x);
((ICounter)x).Increment(); // Modify boxed copy of x
Console.WriteLine(x);
}
static void
Test<Counter>();
}
}
The first call to Increment modifies the value in the variable x. This is not equivalent to the second call to Increment, which modifies the value in a boxed copy of x. Thus, the output of the program is:
0
1
1
20.7.4 Conversions involving type parameters
The following implicit conversions exist for a given type parameter T:
· From T to its effective base class C, from T to any base class of C, and from T to any interface implemented by C. At run-time, if T is a value type, the conversion is executed as a boxing conversion. Otherwise, the conversion is executed as an implicit reference conversion or identity conversion.
· From T to an interface type I in T’s effective interface set and from T to any base interface of I. At run-time, if T is a value type, the conversion is executed as a boxing conversion. Otherwise, the conversion is executed as an implicit reference conversion or identity conversion.
· From T to a type parameter U, provided T depends on U. At run-time, if T is a value type and U is a reference type, the conversion is executed as a boxing conversion. Otherwise, if both T and U are value types, then T and U are necessarily the same type and no conversion is performed. Otherwise, if T is a reference type, then U is necessarily also a reference type and the conversion is executed as an implicit reference conversion or identity conversion.
· From the null type to T, provided T is known to be a reference type.
If T is known to be a reference type, the conversions above are all classified as implicit reference conversions (§6.1.4). If T is not known to be a reference type, the conversions described in the first two bullets above are classified as boxing conversions (§6.1.5).
The following explicit conversions exist for a given type parameter T:
· From the effective base class C of T to T and from any base class of C to T. At run-time, if T is a value type, the conversion is executed as an unboxing conversion. Otherwise, the conversion is executed as an explicit reference conversion or identity conversion.
· From any interface type to T. At run-time, if T is a value type, the conversion is executed as an unboxing conversion. Otherwise, the conversion is executed as an explicit reference conversion or identity conversion.
· From T to any interface-type I provided there is not already an implicit conversion from T to I. At run-time, if T is a value type, the conversion is executed as a boxing conversion followed by an explicit reference conversion. Otherwise, the conversion is executed as an explicit reference conversion or identity conversion.
· From a type parameter U to T, provided T depends on U. At run-time, if T is a value type and U is a reference type, the conversion is executed as an unboxing conversion. Otherwise, if both T and U are value types, then T and U are necessarily the same type and no conversion is performed. Otherwise, if T is a reference type, then U is necessarily also a reference type and the conversion is executed as an explicit reference conversion or identity conversion.
If T is known to be a reference type, the conversions above are all classified as explicit reference conversions (§6.2.3). If T is not known to be a reference type, the conversions described in the first two bullets above are classified as unboxing conversions (§6.2.4).
The above rules do not permit a direct explicit conversion from an unconstrained type parameter to a non-interface type, which might be surprising. The reason for this rule is to prevent confusion and make the semantics of such conversions clear. For example, consider the following declaration:
class X<T>
{
public static long F(T t) {
return (long)t; // Error
}
}
If the direct explicit conversion of t to int were permitted, one might easily expect that X<int>.F(7) would return 7L. However, it would not, because the standard numeric conversions are only considered when the types are known to be numeric at compile time. In order to make the semantics clear, the above example must instead be written:
class X<T>
{
public static long F(T t) {
return (long)(object)t; // Ok, but will only work when T is long
}
}
This code will now compile but executing X<int>.F(7) would then throw an exception at runtime, since a boxed int cannot be converted directly to a long.
20.8 Expressions and statements
The operation of some expressions and statements is modified with generics. This section details those changes.
20.8.1 Object creation expressions
The type of an object creation expression can be a type parameter. When a type parameter is specified as the type in an object creation expression, both of the following conditions must hold, or a compile-time error occurs:
· The argument list must be empty.
· A value type constraint or a constructor constraint must have been specified for the type parameter.
Execution of the object creation expression occurs by creating an instance of the run-time type that the type parameter has been bound to, and invoking the default constructor of that type. The run-time type may be a reference type or a value type.
20.8.2 The typeof operator
The typeof operator is used to obtain the System.Type object for a type.
typeof-expression:
typeof ( type )
typeof ( unbound-type-name )
typeof ( void )
unbound-type-name:
identifier generic-dimension-specifieropt
identifier :: identifier generic-dimension-specifieropt
unbound-type-name . identifier generic-dimension-specifieropt
generic-dimension-specifier:
< commasopt >
commas:
,
commas ,
The first form of typeof-expression consists of a typeof keyword followed by a parenthesized type. The result of an expression of this form is the System.Type object for the indicated type. There is only one System.Type object for any given type. This means that for a type T, typeof(T) == typeof(T) is always true.
The second form of typeof-expression consists of a typeof keyword followed by a parenthesized unbound-type-name. An unbound-type-name is very similar to a type-name (§3.8) except that an unbound-type-name contains generic-dimension-specifiers where a type-name contains type-argument-lists. When the operand of a typeof-expression is a sequence of tokens that satisfies the grammars of both unbound-type-name and type-name, namely when it contains neither a generic-dimension-specifier nor a type-argument-list, the sequence of tokens is considered to be a type-name. The meaning of an unbound-type-name is determined as follows:
· Convert the sequence of tokens to a type-name by replacing each generic-dimension-specifier with a type-argument-list having the same number of commas and the keyword object as each type-argument.
· Evaluate the resulting type-name, while ignoring all type parameter constraints.
· The unbound-type-name resolves to the unbound generic type associated with the resulting constructed type (§20.5).
The result of the typeof-expression is the System.Type object for the resulting unbound generic type.
The third form of typeof-expression consists of a typeof keyword followed by a parenthesized void keyword. The result of an expression of this form is the System.Type object that represents the absence of a type. The type object returned by typeof(void) is distinct from the type object returned for any type. This special type object is useful in class libraries that allow reflection onto methods in the language, where those methods wish to have a way to represent the return type of any method, including void methods, with an instance of System.Type.
The typeof operator can be used on a type parameter. The result is the System.Type object for the run-time type that was bound to the type parameter. The typeof operator can also be used on a constructed type or an unbound generic type (§20.5). The System.Type object for an unbound generic type is not the same as the System.Type object of the instance type. The instance type is always a closed constructed type at run-time so its System.Type object depends on the runtime type arguments in use, while the unbound generic type has no type arguments.
The example
class X<T>
{
public static void PrintTypes() {
Console.WriteLine(typeof(T));
Console.WriteLine(typeof(X<T>));
Console.WriteLine(typeof(X<X<T>>));
Console.WriteLine(typeof(X<>));
}
}
class M
{
static void
X<int>.PrintTypes();
}
}
produces the output:
System.Int32
x`1[System.Int32]
X`1[X`1[System.Int32]]
X`1[T]
Note that the result of typeof(X<>) does not depend on the type argument but the result of typeof(X<T>) does depend on the type argument.
20.8.3 Reference equality operators
The reference type equality operators (§7.9.6) may be used to compare values of a type parameter T if T is known to be a reference type (§20.7).
The use of the reference type equality operators is slightly relaxed to allow one argument to be of a type parameter T and the other argument to be null, as long as T does not have the value type constraint (§20.7). At run-time, if T is a value type, the result of the comparison is false.
The following example checks whether an argument of an unconstrained type parameter type is null.
class C<T>
{
void F(T x) {
if (x == null) throw new ArgumentNullException();
...
}
}
The x == null construct is permitted even though T could represent a value type, and the result is simply defined to be false when T is a value type.
20.8.4 The is operator
The is operator supports open types. In an operation of the form e is T, if either the compile-time type of e or T is an open type, a dynamic type check on the run-time types of e and T is always performed.
See §24.3.5 for further details on changes to the is operator.
20.8.5 The as operator
The as operator can be used with a type parameter T as the right hand side only if T is known to be a reference type (§20.7). This restriction is required because the value null might be returned as a result of the operator.
class X
{
public T F<T>(object o) where T: Attribute {
return o as T; // Ok, T has a class constraint
}
public T G<T>(object o) {
return o as T; // Error, unconstrained T
}
}
See §24.3.6 for further details on changes to the as operator.
20.8.6 Exception statements
The usual rules for throw (§8.9.5) and try (§8.10) statements apply when used with open types:
· The throw statement can be used with an expression whose type is given by a type parameter only if that type parameter has System.Exception (or a subclass thereof) as its effective base class (§20.7).
· The type named in a catch clause may be a type parameter only if that type parameter has System.Exception (or a subclass thereof) as its effective base class (§20.7).
20.8.7 The lock statement
The lock statement may be used with an expression whose type is given by a type parameter, provided the type parameter is known to be a reference type (§20.7).
20.8.8 The using statement
The using statement (§8.13) follows the usual rules: the expression must be implicitly convertible to System.IDisposable. If a type parameter is constrained by System.IDisposable, then expressions of that type may be used with a using statement.
20.8.9 The foreach statement
In a foreach statement of the form
foreach (ElementType element in collection) statement
if the collection expression is a type that does not implement the collection pattern, but does implement the constructed interface System.Collections.Generic.IEnumerable<T> for exactly one type T, then the expansion of the foreach statement is:
IEnumerator<T> enumerator = ((IEnumerable<T>)(collection)).GetEnumerator();
try {
while (enumerator.MoveNext()) {
ElementType element = (ElementType)enumerator.Current;
statement;
}
}
finally {
enumerator.Dispose();
}
20.9 Revised lookup rules
Generics modify some of the basic rules used to look up and bind names. The following sections restate all the basic name lookup rules, taking generics into account.
20.9.1 Namespace and type names
The following replaces §3.8:
Several contexts in a C# program require a namespace-name or a type-name to be specified.
namespace-name:
namespace-or-type-name
type-name:
namespace-or-type-name
namespace-or-type-name:
identifier type-argument-listopt
namespace-or-type-name . identifier type-argument-listop
qualified-alias-member
The qualified-alias-member production is defined in §25.3.1.
A namespace-name is a namespace-or-type-name that refers to a namespace. Following resolution as described below, the namespace-or-type-name of a namespace-name must refer to a namespace, or otherwise a compile-time error occurs. No type arguments (§20.5.1) can be present in a namespace-name (only types can have type arguments).
A type-name is a namespace-or-type-name that refers to a type. Following resolution as described below, the namespace-or-type-name of a type-name must refer to a type, or otherwise a compile-time error occurs.
A namespace-or-type-name has one of four forms:
· I
· I<A1, ..., AK>
· N.I
· N.I<A1, ..., AK>
where I is a single identifier, N is a namespace-or-type-name and <A1, ..., AK> is an optional type-argument-list. When no type-argument-list is specified, consider K to be zero.
The meaning of a namespace-or-type-name is determined as follows:
· If the namespace-or-type-name is of the form I or of the form I<A1, ..., AK>:
o If K is zero and the namespace-or-type-name appears within the body of a generic method declaration (§20.6) and if that declaration includes a type parameter (§20.1.1) with name I, then the namespace-or-type-name refers to that type parameter.
o Otherwise, if the namespace-or-type-name appears within the body of a type declaration, then for each instance type T (§20.1.2), starting with the instance type of that type declaration and continuing with the instance type of each enclosing class or struct declaration (if any):
· If K is zero and the declaration of T includes a type parameter with name I, then the namespace-or-type-name refers to that type parameter.
· Otherwise, if T contains a nested accessible type having name I and K type parameters, then the namespace-or-type-name refers to that type constructed with the given type arguments. If there is more than one such type, the type declared within the more derived type is selected. Note that non-type members (constants, fields, methods, properties, indexers, operators, instance constructors, destructors, and static constructors) and type members with a different number of type parameters are ignored when determining the meaning of the namespace-or-type-name.
o Otherwise, for each namespace N, starting with the namespace in which the namespace-or-type-name occurs, continuing with each enclosing namespace (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:
· If K is zero and I is the name of a namespace in N, then:
o If the location where the namespace-or-type-name occurs is enclosed by a namespace declaration for N and the namespace declaration contains an extern-alias-directive or using-alias-directive that associates the name I with a namespace or type, then the namespace-or-type-name is ambiguous and a compile-time error occurs.
o Otherwise, the namespace-or-type-name refers to the namespace named I in N.
· Otherwise, if N contains an accessible type having name I and K type parameters, then:
o If K is zero and the location where the namespace-or-type-name occurs is enclosed by a namespace declaration for N and the namespace declaration contains an extern-alias-directive or using-alias-directive that associates the name I with a namespace or type, then the namespace-or-type-name is ambiguous and a compile-time error occurs.
o Otherwise, the namespace-or-type-name refers to the type constructed with the given type arguments.
· Otherwise, if the location where the namespace-or-type-name occurs is enclosed by a namespace declaration for N:
o If K is zero and the namespace declaration contains an extern-alias-directive or using-alias-directive that associates the name I with an imported namespace or type, then the namespace-or-type-name refers to that namespace or type.
o Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain exactly one type having name I and K type parameters, then the namespace-or-type-name refers to that type constructed with the given type arguments.
o Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain more than one type having name I and K type parameters, then the namespace-or-type-name is ambiguous and an error occurs.
o Otherwise, the namespace-or-type-name is undefined and a compile-time error occurs.
· Otherwise, the namespace-or-type-name is of the form N.I or of the form N.I<A1, ..., AK>. N is first resolved as a namespace-or-type-name. If the resolution of N is not successful, a compile-time error occurs. Otherwise, N.I or N.I<A1, ..., AK> is resolved as follows:
o If K is zero and N refers to a namespace and N contains a nested namespace with name I, then the namespace-or-type-name refers to that nested namespace.
o Otherwise, if N refers to a namespace and N contains an accessible type having name I and K type parameters, then the namespace-or-type-name refers to that type constructed with the given type arguments.
o Otherwise, if N refers to a (possibly constructed) class or struct type and N contains a nested accessible type having name I and K type parameters, then the namespace-or-type-name refers to that type constructed with the given type arguments. If there is more than one such type, the type declared within the more derived type is selected.
o Otherwise, N.I is an invalid namespace-or-type-name, and a compile-time error occurs.
A namespace-or-type-name is permitted to reference a static class (§25.2) if
· The namespace-or-type-name is the T in a namespace-or-type-name of the form T.I, or
· The namespace-or-type-name is the T in a typeof-expression (§7.5.11) of the form typeof(T).
20.9.2 Member lookup
The following replaces §7.3:
A member lookup is the process whereby the meaning of a name in the context of a type is determined. A member lookup can occur as part of evaluating a simple-name (§20.9.5) or a member-access (§20.9.6) in an expression.
Member lookup considers not only the name of a member but also the number of type parameters the member has and whether the member is accessible. For the purposes of member lookup, generic methods and nested generic types have the number of type parameters indicated in their respective declarations and all other members have zero type parameters.
A member lookup of a name N with K type parameters in a type T is processed as follows:
· First, a set of accessible members named N is determined:
o If T is a type parameter, then the set is the union of the sets of accessible members named N in each of the types specified as a primary constraint or secondary constraint (§20.7) for T, along with the set of accessible members named N in object.
o Otherwise, the set consists of all accessible (§3.5) members named N in T, including inherited members and the accessible members named N in object. If T is a constructed type, the set of members is obtained by substituting type arguments as described in §20.5.4. Members that include an override modifier are excluded from the set.
· Next, if K is zero, all nested types whose declarations include type parameters are removed. If K is not zero, all members with a different number of type parameters are removed. Note that when K is zero, methods having type parameters are not removed, since the type inference process (§20.6.4) might be able to infer the type arguments.
· Next, members that are hidden by other members are removed from the set. For every member S.M in the set, where S is the type in which the member M is declared, the following rules are applied:
o If M is a constant, field, property, event, or enumeration member, then all members declared in a base type of S are removed from the set.
o If M is a type declaration, then all non-types declared in a base type of S are removed from the set, and all type declarations with the same number of type parameters as M declared in a base type of S are removed from the set.
o If M is a method, then all non-method members declared in a base type of S are removed from the set.
· Next, interface members that are hidden by class members are removed from the set. This step only has an effect if T is a type parameter and T has both an effective base class other than object and a non-empty effective interface set (§20.7). For every member S.M in the set, where S is the type in which the member M is declared, the following rules are applied if S is a class declaration other than object:
o If M is a constant, field, property, event, enumeration member, or type declaration, then all members declared in an interface declaration are removed from the set.
o If M is a method, then all non-method members declared in an interface declaration are removed from the set, and all methods with the same signature as M declared in an interface declaration are removed from the set.
· Finally, having removed hidden members, the result of the lookup is determined:
o If the set consists of a single member that is not a method, then this member is the result of the lookup.
o Otherwise, if the set contains only methods, then this group of methods is the result of the lookup.
o Otherwise, the lookup is ambiguous, and a compile-time error occurs.
For member lookups in types other than type parameters and interfaces, and member lookups in interfaces that are strictly single-inheritance (each interface in the inheritance chain has exactly zero or one direct base interface), the effect of the lookup rules is simply that derived members hide base members with the same name or signature. Such single-inheritance lookups are never ambiguous. The ambiguities that can possibly arise from member lookups in multiple-inheritance interfaces are described in §13.2.5.
20.9.3 Applicable function member
The following paragraph is removed from §7.4.2.1:
· If the class, struct, or interface in which the function member is declared already contains another applicable function member with the same signature as the expanded form, the expanded form is not applicable.
Thus, a function member can be applicable in its expanded form even if a declared member with the same signature exists. The potential ambiguity is resolved by the tie-breaking rules described in the following section.
20.9.4 Better function member
The following replaces §7.4.2.2:
Given an argument list A with a sequence of argument types {A1, A2, …, AN} and two applicable function members MP and MQ with parameter types {P1, P2, …, PN} and {Q1, Q2, …, QN}, after expansion and type argument substitution, MP is defined to be a better function member than MQ if
· For each argument, the implicit conversion from AX to PX is not worse than the implicit conversion from AX to QX, and
· For at least one argument, the conversion from AX to PX is better than the conversion from AX to QX.
In case the parameter type sequences {P1, P2, …, PN} and {Q1, Q2, …, QN} are identical, the following tie-breaking rules are applied, in order, to determine the better function member.
· If MP is a non-generic method and MQ is a generic method, then MP is better than MQ.
· Otherwise, if MP is applicable in its normal form and MQ has a params array and is applicable only in its expanded form, then MP is better than MQ.
· Otherwise, if MP has fewer declared parameters than MQ, then MP is better than MQ. This can occur if both methods have params arrays and are applicable only in their expanded forms.
· Otherwise, if MP has more specific parameter types than MQ, then MP is better than MQ. Let {R1, R2, …, RN} and {S1, S2, …, SN} represent the uninstantiated and unexpanded parameter types of MP and MQ. MP’s parameter types are more specific than MQ’s if, for each parameter, RX is not less specific than SX, and, for at least one parameter, RX is more specific than SX:
o A type parameter is less specific than a non-type parameter.
o Recursively, a constructed type is more specific than another constructed type (with the same number of type arguments) if at least one type argument is more specific and no type argument is less specific than the corresponding type argument in the other.
o An array type is more specific than another array type (with the same number of dimensions) if the element type of the first is more specific than the element type of the second.
· Otherwise, neither method is better.
20.9.5 Simple names
The following replaces §7.5.2:
A simple-name consists of an identifier, optionally followed by a type argument list:
simple-name:
identifier type-argument-listopt
A simple-name is either of the form I or of the form I<A1, ..., AK>, where I is a single identifier and <A1, ..., AK> is an optional type-argument-list. When no type-argument-list is specified, consider K to be zero. The simple-name is evaluated and classified as follows:
· If K is zero and the simple-name appears within a block and if the block’s (or an enclosing block’s) local variable declaration space (§3.3) contains a local variable, parameter or constant with name I, then the simple-name refers to that local variable, parameter or constant and is classified as a variable or value.
· If K is zero and the simple-name appears within the body of a generic method declaration and if that declaration includes a type parameter with name I, then the simple-name refers to that type parameter.
· Otherwise, for each instance type T (§20.1.2), starting with the instance type of the immediately enclosing type declaration and continuing with the instance type of each enclosing class or struct declaration (if any):
o If K is zero and the declaration of T includes a type parameter with name I, then the simple-name refers to that type parameter.
o Otherwise, if a member lookup (§20.9.2) of I in T with K type arguments produces a match:
· If T is the instance type of the immediately enclosing class or struct type and the lookup identifies one or more methods, the result is a method group with an associated instance expression of this. If a type argument list was specified, it is used in calling a generic method (§20.6.3).
· Otherwise, if T is the instance type of the immediately enclosing class or struct type, if the lookup identifies an instance member, and if the reference occurs within the block of an instance constructor, an instance method, or an instance accessor, the result is the same as a member access (§20.9.6) of the form this.I. This can only happen when K is zero.
· Otherwise, the result is the same as a member access (§20.9.6) of the form T.I or T.I<A1, ..., AK>. In this case, it is a compile-time error for the simple-name to refer to an instance member.
· Otherwise, for each namespace N, starting with the namespace in which the simple-name occurs, continuing with each enclosing namespace (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:
o If K is zero and I is the name of a namespace in N, then:
· If the location where the simple-name occurs is enclosed by a namespace declaration for N and the namespace declaration contains an extern-alias-directive or using-alias-directive that associates the name I with a namespace or type, then the simple-name is ambiguous and a compile-time error occurs.
· Otherwise, the simple-name refers to the namespace named I in N.
o Otherwise, if N contains an accessible type having name I and K type parameters, then:
· If K is zero and the location where the simple-name occurs is enclosed by a namespace declaration for N and the namespace declaration contains an extern-alias-directive or using-alias-directive that associates the name I with a namespace or type, then the simple-name is ambiguous and a compile-time error occurs.
· Otherwise, the namespace-or-type-name refers to the type constructed with the given type arguments.
o Otherwise, if the location where the simple-name occurs is enclosed by a namespace declaration for N:
· If K is zero and the namespace declaration contains an extern-alias-directive or using-alias-directive that associates the name I with an imported namespace or type, then the simple-name refers to that namespace or type.
· Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain exactly one type having name I and K type parameters, then the simple-name refers to that type constructed with the given type arguments.
· Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain more than one type having name I and K type parameters, then the simple-name is ambiguous and an error occurs.
Note that this entire step is exactly parallel to the corresponding step in the processing of a namespace-or-type-name (§3.8 and §20.9.1).
· Otherwise, the simple-name is undefined and a compile-time error occurs.
20.9.6 Member access
The following replaces §7.5.4:
A member-access consists of a primary-expression, a predefined-type, or a qualified-alias-member, followed by a “.” token, followed by an identifier, optionally followed by a type-argument-list.
member-access:
primary-expression . identifier type-argument-listopt
predefined-type . identifier type-argument-listopt
qualified-alias-member . identifier
predefined-type: one of
bool byte char decimal double float int long
object sbyte short string uint ulong ushort
The qualified-alias-member production is defined in §25.3.1.
A member-access is either of the form E.I or of the form E.I<A1, ..., AK>, where E is a primary-expression, I is a single identifier and <A1, ..., AK> is an optional type-argument-list. When no type-argument-list is specified, consider K to be zero. The member-access is evaluated and classified as follows:
· If K is zero and E is a namespace and E contains a nested namespace with name I, then the result is that namespace.
· Otherwise, if E is a namespace and E contains an accessible type having name I and K type parameters, then the result is that type constructed with the given type arguments.
· If E is a predefined-type or a primary-expression classified as a type, if E is not a type parameter, and if a member lookup (§20.9.2) of I in E with K type parameters produces a match, then E.I is evaluated and classified as follows:
o If I identifies a type, then the result is that type constructed with the given type arguments.
o If I identifies one or more methods, then the result is a method group with no associated instance expression. If a type argument list was specified, it is used in calling a generic method (§20.6.3).
o If I identifies a static property, then the result is a property access with no associated instance expression.
o If I identifies a static field:
· If the field is readonly and the reference occurs outside the static constructor of the class or struct in which the field is declared, then the result is a value, namely the value of the static field I in E.
· Otherwise, the result is a variable, namely the static field I in E.
o If I identifies a static event:
· If the reference occurs within the class or struct in which the event is declared, and the event was declared without event-accessor-declarations (§10.7), then E.I is processed exactly as if I were a static field.
· Otherwise, the result is an event access with no associated instance expression.
o If I identifies a constant, then the result is a value, namely the value of that constant.
o If I identifies an enumeration member, then the result is a value, namely the value of that enumeration member.
o Otherwise, E.I is an invalid member reference, and a compile-time error occurs.
· If E is a property access, indexer access, variable, or value, the type of which is T, and a member lookup (§20.9.2) of I in T with K type arguments produces a match, then E.I is evaluated and classified as follows:
o First, if E is a property or indexer access, then the value of the property or indexer access is obtained (§7.1.1) and E is reclassified as a value.
o If I identifies one or more methods, then the result is a method group with an associated instance expression of E. If a type argument list was specified, it is used in calling a generic method (§20.6.3).
o If I identifies an instance property, then the result is a property access with an associated instance expression of E.
o If T is a class-type and I identifies an instance field of that class-type:
· If the value of E is null, then a System.NullReferenceException is thrown.
· Otherwise, if the field is readonly and the reference occurs outside an instance constructor of the class in which the field is declared, then the result is a value, namely the value of the field I in the object referenced by E.
· Otherwise, the result is a variable, namely the field I in the object referenced by E.
o If T is a struct-type and I identifies an instance field of that struct-type:
· If E is a value, or if the field is readonly and the reference occurs outside an instance constructor of the struct in which the field is declared, then the result is a value, namely the value of the field I in the struct instance given by E.
· Otherwise, the result is a variable, namely the field I in the struct instance given by E.
o If I identifies an instance event:
· If the reference occurs within the class or struct in which the event is declared, and the event was declared without event-accessor-declarations (§10.7), then E.I is processed exactly as if I was an instance field.
· Otherwise, the result is an event access with an associated instance expression of E.
· Otherwise, E.I is an invalid member reference, and a compile-time error occurs.
20.9.7 Method invocations
The following replaces the part of §7.5.5.1 that describes compile-time processing of a method invocation:
The compile-time processing of a method invocation of the form M(A), where M is a method group (possibly including a type-argument-list), and A is an optional argument-list, consists of the following steps:
· The set of candidate methods for the method invocation is constructed. For each method F associated with the method group M:
o If F is non-generic, F is a candidate when:
· M has no type argument list, and
· F is applicable with respect to A (§7.4.2.1).
o If F is generic and M has no type argument list, F is a candidate when:
· Type inference (§20.6.4) succeeds, inferring a list of type arguments for the call, and
· Once the inferred type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§20.7.1), and the parameter list of F is applicable with respect to A (§7.4.2.1).
o If F is generic and M includes a type argument list, F is a candidate when:
· F has the same number of method type parameters as were supplied in the type argument list, and
· Once the type arguments are substituted for the corresponding method type parameters, all constructed types in the parameter list of F satisfy their constraints (§20.7.1), and the parameter list of F is applicable with respect to A (§7.4.2.1).
· The set of candidate methods is reduced to contain only methods from the most derived types: For each method C.F in the set, where C is the type in which the method F is declared, all methods declared in a base type of C are removed from the set. Furthermore, if C is a class type other than object, all methods declared in an interface type are removed from the set. (This latter rule only has affect when the method group was the result of a member lookup on a type parameter having an effective base class other than object and a non-empty effective interface set.)
· If the resulting set of candidate methods is empty, then no applicable methods exist, and a compile-time error occurs. If the candidate methods are not all declared in the same type, the method invocation is ambiguous, and a compile-time error occurs. (This latter situation can only occur for an invocation of a method in an interface that has multiple direct base interfaces, as described in §13.2.5, or for an invocation of a method on a type parameter.)
· The best method of the set of candidate methods is identified using the overload resolution rules of §7.4.2. If a single best method cannot be identified, the method invocation is ambiguous, and a compile-time error occurs. When performing overload resolution, the parameters of a generic method are considered after substituting the type arguments (supplied or inferred) for the corresponding method type parameters.
· Final validation of the chosen best method is performed:
o The method is validated in the context of the method group: If the best method is a static method, the method group must have resulted from a simple-name or a member-access through a type. If the best method is an instance method, the method group must have resulted from a simple-name, a member-access through a variable or value, or a base-access. If neither of these requirements is true, a compile-time error occurs.
o If the best method is a generic method, the type arguments (supplied or inferred) are checked against the constraints (§20.7.1) declared on the generic method. If any type argument does not satisfy the corresponding constraint(s) on the type parameter, a compile-time error occurs.
Once a method has been selected and validated at compile-time by the above steps, the actual run-time invocation is processed according to the rules of function member invocation described in §7.4.3.
20.10 Right-shift grammar changes
The syntax for generics uses the < and > characters to delimit type parameters and type arguments (similar to the syntax used in C++ for templates). Constructed types sometimes nest, as in List<Nullable<int>>, but there is a subtle grammatical problem with such constructs: the lexical grammar will combine the last two characters of this construct into the single token >> (the right shift operator), rather than producing two > tokens, which the syntactic grammar would require. Although a possible solution is to put a space in between the two > characters, this is awkward and confusing, and does not add to the clarity of the program in any way.
In order to allow these natural constructs, and to maintain a simple lexical grammar, the >> and >= tokens are removed from the lexical grammar and replaced with right-shift and right-shift-assignment productions. (Note also that two new tokens, ?? and ::, are added to the lexical grammar, the purposes of which are described in §24.3.9 and §25.3, respectively.)
operator-or-punctuator: one of
{ } [ ] ( ) . , : ;
+ - * / % & | ^ ! ~
= < > ? ?? :: ++ -- && ||
-> == != <= >= += -= *= /= %=
&= |= ^= << <<=
right-shift:
> >
right-shift-assignment:
> >=
Unlike other productions in the syntactic grammar, no characters of any kind (not even whitespace) are allowed between the tokens in the right-shift and right-shift-assignment productions.
The following productions are modified to use right-shift and right-shift-assignment:
shift-expression:
additive-expression
shift-expression << additive-expression
shift-expression right-shift additive-expression
assignment-operator:
=
+=
-=
*=
/=
%=
&=
|=
^=
<<=
right-shift-assignment
overloadable-binary-operator:
+
-
*
/
%
&
|
^
<<
right-shift
==
!=
>
<
>=
<=
21. Anonymous methods
21.1 Anonymous method expressions
An anonymous-method-expression defines an anonymous method and evaluates to a special value referencing the method:
primary-no-array-creation-expression:
…
anonymous-method-expression
anonymous-method-expression:
delegate anonymous-method-signatureopt block
anonymous-method-signature:
( anonymous-method-parameter-listopt )
anonymous-method-parameter-list:
anonymous-method-parameter
anonymous-method-parameter-list , anonymous-method-parameter
anonymous-method-parameter:
parameter-modifieropt type identifier
An anonymous-method-expression is classified as a value with special conversion rules (§21.3). The value does not have a type but can be implicitly converted to a compatible delegate type.
The anonymous-method-expression defines a new declaration space for parameters, locals and constants and a new declaration space for labels (§3.3).
21.2 Anonymous method signatures
The optional anonymous-method-signature defines the names and types of the formal parameters for the anonymous method. The scope of the parameters of the anonymous method is the block. It is a compile-time error for the name of a parameter of the anonymous method to match the name of a local variable, local constant or parameter whose scope includes the anonymous-method-expression.
If an anonymous-method-expression has an anonymous-method-signature, then the set of compatible delegate types is restricted to those that have the same parameter types and modifiers in the same order (§21.3). In contrast to method group conversions (§21.9), contra-variance of anonymous method parameter types is not supported. If an anonymous-method-expression doesn’t have an anonymous-method-signature, then the set of compatible delegate types is restricted to those that have no out parameters.
Note that an anonymous-method-signature cannot include attributes or a parameter array. Nevertheless, an anonymous-method-signature may be compatible with a delegate type whose parameter list contains a parameter array.
21.3 Anonymous method conversions
An anonymous-method-expression is classified as a value with no type. An anonymous-method-expression may be used in a delegate-creation-expression (§21.10). All other valid uses of an anonymous-method-expression depend on the implicit conversions defined here.
An implicit conversion (§6.1) exists from an anonymous-method-expression to any compatible delegate type. If D is a delegate type, and A is an anonymous-method-expression, then D is compatible with A if and only if the following two conditions are met.
· First, the parameter types of D must be compatible with A:
o If A does not contain an anonymous-method-signature, then D may have zero or more parameters of any type, as long as no parameter of D has the out parameter modifier.
o If A has an anonymous-method-signature, then D must have the same number of parameters, each parameter of A must be of the same type as the corresponding parameter of D, and the presence or absence of the ref or out modifier on each parameter of A must match the corresponding parameter of D. Whether the last parameter of D is a parameter-array is not relevant to the compatibility of A and D.
· Second, the return type of D must be compatible with A. For these rules, A is not considered to contain the block of any other anonymous methods.
o If D is declared with a void return type, then any return statement contained in A may not specify an expression.
o If D is declared with a return type of R, then any return statement contained in A must specify an expression which is implicitly convertible (§6.1) to R. Furthermore, the end-point of the block of A must not be reachable.
Besides the implicit conversions to compatible delegate types, no other conversions exist from an anonymous-method-expression, even to the type object.
The following examples illustrate these rules:
delegate void D(int x);
D d1 = delegate { }; // Ok
D d2 = delegate() { }; // Error, signature mismatch
D d3 = delegate(long x) { }; // Error, signature mismatch
D d4 = delegate(int x) { }; // Ok
D d5 = delegate(int x) { return; }; // Ok
D d6 = delegate(int x) { return x; }; // Error, return type mismatch
delegate void E(out int x);
E e1 = delegate { }; // Error, E has an out parameter
E e2 = delegate(out int x) { x = 1; }; // Ok
E e3 = delegate(ref int x) { x = 1; }; // Error, signature mismatch
delegate int P(params int[] a);
P p1 = delegate { }; // Error, end of block reachable
P p2 = delegate { return; }; // Error, return type mismatch
P p3 = delegate { return 1; }; // Ok
P p4 = delegate { return "Hello"; }; // Error, return type mismatch
P p5 = delegate(int[] a) { // Ok
return a[0];
};
P p6 = delegate(params int[] a) { // Error, params modifier
return a[0];
};
P p7 = delegate(int[] a) { // Error, return type mismatch
if (a.Length > 0) return a[0];
return "Hello";
};
delegate object Q(params int[] a);
Q q1 = delegate(int[] a) { // Ok
if (a.Length > 0) return a[0];
return "Hello";
};
21.4 Anonymous method blocks
The block of an anonymous-method-expression is subject to the following rules:
· If the anonymous method includes a signature, the parameters specified in the signature are available in the block. If the anonymous method has no signature it can be converted to a
· Except for ref or out parameters specified in the signature (if any) of the nearest enclosing anonymous method, it is a compile-time error for the block to access a ref or out parameter.
· When the type of this is a struct type, it is a compile-time error for the block to access this. This is true whether the access is explicit (as in this.x) or implicit (as in x where x is an instance member of the struct). This rule simply prohibits such access and does not affect whether member lookup results in a member of the struct.
· The block has access to the outer variables (§21.5) of the anonymous method. Access of an outer variable will reference the instance of the variable that is active at the time the anonymous-method-expression is evaluated (§21.6).
· It is a compile-time error for the block to contain a goto statement, break statement, or continue statement whose target is outside the block or within the block of a contained anonymous method.
· A return statement in the block returns control from an invocation of the nearest enclosing anonymous method, not from the enclosing function member. An expression specified in a return statement must be compatible with the delegate type to which the nearest enclosing anonymous-method-expression is converted (§21.3).
It is explicitly unspecified whether there is any way to execute the block of an anonymous method other than through evaluation and invocation of the anonymous-method-expression. In particular, the compiler may choose to implement an anonymous method by synthesizing one or more named methods or types. The names of any such synthesized elements must be in the space reserved for compiler use (that is, the names must contain two consecutive underscore characters).
21.5 Outer variables
Any local variable, value parameter, or parameter array whose scope includes the anonymous-method-expression is called an outer variable of the anonymous-method-expression. In an instance function member of a class, the this value is considered a value parameter and is an outer variable of any anonymous-method-expression contained within the function member.
21.5.1 Captured outer variables
When an outer variable is referenced by an anonymous method, the outer variable is said to have been captured by the anonymous method. Ordinarily, the lifetime of a local variable is limited to execution of the block or statement with which it is associated (§5.1.7). However, the lifetime of a captured outer variable is extended at least until the delegate referring to the anonymous method becomes eligible for garbage collection.
In the example
using System;
class Test
{
static D F() {
int x = 0;
D result = delegate { return ++x; }
return result;
}
static void
D d = F();
Console.WriteLine(d());
Console.WriteLine(d());
Console.WriteLine(d());
}
}
the local variable x is captured by the anonymous method, and the lifetime of x is extended at least until the delegate returned from F becomes eligible for garbage collection (which doesn’t happen until the very end of the program). Since each invocation of the anonymous method operates on the same instance of x, the output of the example is:
1
2
3
When a local variable or a value parameter is captured by an anonymous method, the local variable or parameter is no longer considered to be a fixed variable (§18.3), but is instead considered to be a moveable variable. Thus any unsafe code that takes the address of a captured outer variable must first use the fixed statement to fix the variable.
21.5.2 Instantiation of local variables
A local variable is considered to be instantiated when execution enters the scope of the variable. For example, when the following method is invoked, the local variable x is instantiated and initialized three times—once for each iteration of the loop.
static void F() {
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
...
}
}
However, moving the declaration of x outside the loop results in a single instantiation of x:
static void F() {
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
...
}
}
Ordinarily, there is no way to observe exactly how often a local variable is instantiated—because the lifetimes of the instantiations are disjoint, it is possible for each instantation to simply use the same storage location. However, when an anonymous method captures a local variable, the effects of instantiation become apparent.
The example
using System;
delegate void D();
class Test
{
static D[] F() {
D[] result = new D[3];
for (int i = 0; i < 3; i++) {
int x = i * 2 + 1;
result[i] =
}
return result;
}
static void
foreach (D d in F()) d();
}
}
produces the output:
1
3
5
However, when the declaration of x is moved outside the loop:
static D[] F() {
D[] result = new D[3];
int x;
for (int i = 0; i < 3; i++) {
x = i * 2 + 1;
result[i] =
}
return result;
}
the output is:
5
5
5
Note that the three delegates created in the version of F directly above will be equal according to the equality operator (§21.7). Furthermore, note that the compiler is permitted (but not required) to optimize the three instantiations into a single delegate instance (§21.6).
It is possible for anonymous method delegates to share some captured variables yet have separate instances of others. For example, if F is changed to
static D[] F() {
D[] result = new D[3];
int x = 0;
for (int i = 0; i < 3; i++) {
int y = 0;
result[i] =
}
return result;
}
the three
1 1
2 1
3 1
Separate anonymous methods can capture the same instance of an outer variable. In the example:
using System;
delegate void Setter(int value);
class Test
{
static void
int x = 0;
Setter s =
Getter g = delegate { return x; };
s(5);
Console.WriteLine(g());
s(10);
Console.WriteLine(g());
}
}
the two anonymous methods capture the same instance of the local variable x, and they can thus “communicate” through that variable. The output of the example is:
5
10
21.6 Anonymous method evaluation
The run-time evaluation of an anonymous-method-expression produces a delegate instance which references the anonymous method and the (possibly empty) set of captured outer variables that are active at the time of the evaluation. When a delegate resulting from an anonymous-method-expression is invoked, the body of the anonymous method is executed. The code in the body is executed using the set of captured outer variables referenced by the delegate.
The invocation list of a
Evaluation of sematically identical anonymous-method-expressions with the same (possibly empty) set of captured outer variable instances is permitted (but not required) to return the same
delegate double Function(double x);
class Test
{
static double[] Apply(double[] a, Function f) {
double[] result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
static void F(double[] a, double[] b) {
a = Apply(a, delegate(double x) { return Math.Sin(x); });
b = Apply(b, delegate(double y) { return Math.Sin(y); });
...
}
}
Since the two anonymous method delegates have the same (empty) set of captured outer variables, and since the anonymous methods are semantically identical, the compiler is permitted to have the
21.7 Delegate instance equality
The following rules govern the results produced by the equality operators (§7.9.8) and the Object.Equals method for anonymous method delegate instances:
· Delegate instances produced from evaluation of semantically identical anonymous-method-expressions with the same (possibly empty) set of captured outer variable instances are permitted (but not required) to be equal.
· Delegate instances produced from evaluation of semantically different anonymous-method-expressions or having different sets of captured outer variable instances are
21.8 Definite assignment
The definite assignment state of a parameter of an anonymous method is the same as for a parameter of a named method. That is, reference parameters and value parameters are initially definitely assigned and output parameters are initially unassigned. Furthermore, output parameters must be definitely assigned before the anonymous method returns normally (§5.1.6).
The definite assignment state of an outer variable v on the control transfer to the block of an anonymous-method-expression is the same as the definite assignment state of v before the anonymous-method-expression. That is, definite assignment of outer variables is inherited from the context of the anonymous-method-expression. Within the block of an anonymous-method-expression, definite assignment evolves as in a normal block (§5.3.3).
The definite assignment state of a variable v after an anonymous-method-expression is the same as its definite assignment state before the anonymous-method-expression.
The example
delegate bool Filter(int i);
void F() {
int max;
// Error, max is not definitely assigned
Filter f = delegate(int n) { return n < max; }
max = 5;
DoWork(f);
}
generates a compile-time error since max is not definitely assigned where the anonymous method is declared. The example
delegate void D();
void F() {
int n;
D d = delegate { n = 1; };
d();
// Error, n is not definitely assigned
Console.WriteLine(n);
}
also generates a compile-time error since the assignment to n in the anonymous method has no affect on the definite assignment state of n outside the anonymous method.
21.9 Method group conversions
Similar to the implicit anonymous method conversions described in §21.3, an implicit conversion (§6.1) exists from a method group (§7.1) to a compatible delegate type. Given a delegate type D and an expression E that is classified as a method group, an implicit conversion exists from E to D if E contains at least one method that is applicable in its normal form (§7.4.2.1) to an argument list having types and modifiers matching the parameter types and modifiers of D.
The compile-time application of a conversion from a method group E to a delegate type D is described in the following. Note that the existence of an implicit conversion from E to D does not guarantee that the compile-time application of the conversion will succeed without error.
· A single method M is selected corresponding to a method invocation (§20.9.7) of the form E(A), with the following modifications:
o The parameter types and modifiers (ref or out) of D are used as the argument types and modifiers of the argument list A.
o The candidate methods considered are only those methods that are applicable in their normal form (§7.4.2.1), not those applicable only in their expanded form.
o If the algorithm of §20.9.7 produces an error, then a compile-time error occurs. Otherwise the algorithm produces a single best method M having the same number of parameters as D.
· The selected method M must be consistent (as described below) with the delegate type D, or otherwise, a compile-time error occurs.
· If the selected method M is an instance method, the instance expression associated with E determines the target object of the delegate.
· The result of the conversion is a value of type D, namely a newly created delegate that refers to the selected method and target object.
A method M is consistent with a
· D and M have the same number of parameters, and each parameter in D has the same modifiers as the corresponding parameter in M.
· For each value parameter (a parameter with no ref or out modifier), an identity conversion (§6.1.1) or implicit reference conversion (§6.1.4) exists from the parameter type in D to the corresponding parameter type in M.
· For each ref or out parameter, the parameter type in D is the same as the parameter type in M.
· An identity or implicit reference conversion exists from the return type of M to the return type of D.
The above rules of delegate consistency replace, and are more permissive than, the former rules of delegate compatibility described in §15.1.
The following example demonstrates method group conversions:
delegate string D1(object o);
delegate object D2(string s);
delegate string D3(int i);
class Test
{
static string F(object o) {...}
static void G() {
D1 d1 = F; // Ok
D2 d2 = F; // Ok
D3 d3 = F; // Error
}
}
The assignment to d1 implicitly converts the method group F to a value of type D1. Previously, it was necessary to write new D1(F) to produce the delegate value. While that is still permitted, it is no longer a requirement.
The assignment to d2 shows how it is possible to
The assignment to d3 shows how parameter and return types of the
As with all other implicit and explicit conversions, the cast operator can be used to explicitly perform a method group conversion. Thus, the example
object obj = new EventHandler(myDialog.OkClick);
could instead be written
object obj = (EventHandler)myDialog.OkClick;
Method groups and anonymous method expressions may influence overload resolution, but do not participate in type inferencing. See §20.6.4 for
21.10 Delegate creation expressions
Delegate creation expressions (§7.5.10.3) are extended to permit the argument to be an expression classified as a method group, an expression classified as an anonymous method, or a value of a
The compile-time processing of a delegate-creation-expression of the form new D(E), where D is a delegate-type and E is an expression, consists of the following steps:
· If E is a method group, a method group conversion (§21.9) must exist from E to D, and the
· If E is an anonymous method, an anonymous method conversion (§21.3) must exist from E to D, and the
· If E is a value of a delegate type, the method signature of E must be consistent (§21.9) with D, and the result is a reference to a newly created
21.11 Implementation example
This section describes a possible implementation of anonymous methods in terms of standard C# constructs. The implementation described here is based on the same principles used by the Microsoft C# compiler, but it is by no means a mandated implementation, nor is it the only one possible.
The remainder of this section gives several examples of code that contains anonymous methods with different characteristics. For each example, a corresponding translation to code that uses only standard C# constructs is provided. In the examples, the identifier D is assumed by represent the following
public
The simplest form of an anonymous method is one that captures no outer variables:
class Test
{
static void F() {
D d =
}
}
This can be translated to a delegate instantiation that references a compiler generated static method in which the code of the anonymous method is placed:
class Test
{
static void F() {
D d = new D(__Method1);
}
static void __Method1() {
Console.WriteLine("test");
}
}
In the following example, the anonymous method references instance members of this:
class Test
{
int x;
void F() {
D d =
}
}
This can be translated to a compiler generated instance method containing the code of the anonymous method:
class Test
{
int x;
void F() {
D d = new D(__Method1);
}
void __Method1() {
Console.WriteLine(x);
}
}
In this example, the anonymous method captures a local variable:
class Test
{
void F() {
int y = 123;
D d =
}
}
The lifetime of the local variable must now be extended to at least the lifetime of the anonymous method
class Test
{
void F() {
__Locals1 __locals1 = new __Locals1();
__locals1.y = 123;
D d = new D(__locals1.__Method1);
}
class __Locals1
{
public int y;
public void __Method1() {
Console.WriteLine(y);
}
}
}
Finally, the following anonymous method captures this as well as two local variables with different lifetimes:
class Test
{
int x;
void F() {
int y = 123;
for (int i = 0; i < 10; i++) {
int z = i * 2;
D d =
}
}
}
Here, a compiler generated class is created for each statement block in which locals are captured such that the locals in the different blocks can have independent lifetimes. An instance of __Locals2, the compiler generated class for the inner statement block, contains the local variable z and a field that references an instance of __Locals1. An instance of __Locals1, the compiler generated class for the outer statement block, contains the local variable y and a field that references this of the enclosing function member. With these data structures it is possible to reach all captured outer variables through an instance of __Local2, and the code of the anonymous method can thus be implemented as an instance method of that class.
class Test
{
void F() {
__Locals1 __locals1 = new __Locals1();
__locals1.__this = this;
__locals1.y = 123;
for (int i = 0; i < 10; i++) {
__Locals2 __locals2 = new __Locals2();
__locals2.__locals1 = __locals1;
__locals2.z = i * 2;
D d = new D(__locals2.__Method1);
}
}
class __Locals1
{
public Test __this;
public int y;
}
class __Locals2
{
public __Locals1 __locals1;
public int z;
public void __Method1() {
Console.WriteLine(__locals1.__this.x + __locals1.y + z);
}
}
}
22. Iterators
22.1 Iterator blocks
An iterator block is a block (§8.2) that yields an ordered sequence of values. An iterator block is distinguished from a normal statement block by the presence of one or more yield statements.
· The yield return statement produces the next value of the iteration.
· The yield break statement
An iterator block may be used as a method-body, operator-body or accessor-body as long as the return type of the corresponding function member is one of the enumerator interfaces (§22.1.1) or one of the enumerable interfaces (§22.1.2).
Iterator blocks are not a distinct element in the C# grammar. They are restricted in several ways and have a major effect on the semantics of a function member declaration; however, grammatically, they are just blocks.
When a function member is implemented using an iterator block, it is a compile-time error for the formal parameter list of the function member to specify any ref or out parameters.
It is a compile-time error for a return statement to appear in an iterator block (but yield return statements are permitted).
It is a compile-time error for an iterator block to contain an unsafe context (§18.1). An iterator block always defines a safe context, even when its declaration is nested in an unsafe context.
22.1.1 Enumerator interfaces
The enumerator interfaces are the non-generic interface System.Collections.IEnumerator and all instantiations of the generic interface System.Collections.Generic.IEnumerator<T>. For the sake of brevity, in this chapter these interfaces are referenced as IEnumerator and IEnumerator<T>, respectively.
22.1.2 Enumerable interfaces
The enumerable interfaces are the non-generic interface System.Collections.IEnumerable and all instantiations of the generic interface System.Collections.Generic.IEnumerable<T>. For the sake of brevity, in this chapter these interfaces are referenced as IEnumerable and IEnumerable<T>, respectively.
22.1.3 Yield type
An iterator block produces a sequence of values, all of the same type. This type is called the yield type of the iterator block.
· The yield type of an iterator block used to implement a function member that returns IEnumerator or IEnumerable is object.
· The yield type of an iterator block used to implement a function member that returns IEnumerator<T> or IEnumerable<T> is T.
22.1.4 This access
Within an iterator block of an instance member of a class, the expression this is classified as a value. The type of the value is the class within which the usage occurs, and the value is a reference to the object for which the member was invoked.
Within an iterator block of an instance member of a struct, the expression this is classified as a variable. The type of the variable is the struct within which the usage occurs. The variable represents a copy of the struct for which the member was invoked. The this variable in an iterator block of an instance member of a struct behaves exactly the same as a value parameter of the struct type. Note that this is different than in a non-iterator function member body.
22.2 Enumerator objects
When a function member returning an enumerator interface type is implemented using an iterator block, invoking the function member does not immediately execute the code in the iterator block. Instead, an enumerator object is created and returned. This object encapsulates the code specified in the iterator block, and execution of the code in the iterator block occurs when the enumerator object’s MoveNext method is invoked. An enumerator object has the following characteristics:
· It implements IEnumerator and IEnumerator<T>, where T is the yield type of the iterator block.
· It implements System.IDisposable.
· It is initialized with a copy of the argument values (if any) and instance value passed to the function member.
· It has four potential states, before, running, suspended, and after, and is initially in the before state.
An enumerator object is typically an instance of a compiler-generated enumerator class that encapsulates the code in the iterator block and implements the enumerator interfaces, but other methods of implementation are possible. If an enumerator class is generated by the compiler, that class will be nested, directly or indirectly, in the class containing the function member, it will have private accessibility, and it will have a name reserved for compiler use (§2.4.2).
An enumerator object may implement more interfaces than those specified above.
The following sections describe the exact behavior of the MoveNext, Current, and Dispose members of the IEnumerable and IEnumerable<T> interface implementations provided by an enumerator object.
Note that enumerator objects do not support the IEnumerator.Reset method. Invoking this method causes a System.NotSupportedException to be thrown.
22.2.1 The MoveNext method
The MoveNext method of an enumerator object encapsulates the code of an iterator block. Invoking the MoveNext method executes code in the iterator block and sets the Current property of the enumerator object as appropriate. The precise action performed by MoveNext depends on the state of the enumerator object when MoveNext is invoked:
· If the state of the enumerator object is before, invoking MoveNext:
o Changes the state to running.
o Initializes the parameters (including this) of the iterator block to the argument values and instance value saved when the enumerator object was initialized.
o Executes the iterator block from the beginning until execution is interrupted (as described below).
· If the state of the enumerator object is running, the result of invoking MoveNext is unspecified.
· If the state of the enumerator object is suspended, invoking MoveNext:
o Changes the state to running.
o Restores the values of all local variables and parameters (including this) to the values saved when execution of the iterator block was last suspended. Note that the contents of any objects referenced by these variables may have changed since the previous call to MoveNext.
o Resumes execution of the iterator block immediately following the yield return statement that caused the suspension of execution and continues until execution is interrupted (as described below).
· If the state of the enumerator object is after, invoking MoveNext returns false.
When MoveNext executes the iterator block, execution can be interrupted in four ways: By a yield return statement, by a yield break statement, by encountering the end of the iterator block, and by an exception being thrown and propagated out of the iterator block.
· When a yield return statement is encountered (§22.4):
o The expression given in the statement is evaluated, implicitly converted to the yield type, and assigned to the Current property of the enumerator object.
o Execution of the iterator body is suspended. The values of all local variables and parameters (including this) are saved, as is the location of this yield return statement. If the yield return statement is within one or more try blocks, the associated finally blocks are not executed at this time.
o The state of the enumerator object is changed to suspended.
o The MoveNext method returns true to its caller, indicating that the iteration successfully advanced to the next value.
· When a yield break statement is encountered (§22.4):
o If the yield break statement is within one or more try blocks, the associated finally blocks are executed.
o The state of the enumerator object is changed to after.
o The MoveNext method returns false to its caller, indicating that the iteration is complete.
· When the end of the iterator body is encountered:
o The state of the enumerator object is changed to after.
o The MoveNext method returns false to its caller, indicating that the iteration is complete.
· When an exception is thrown and propagated out of the iterator block:
o Appropriate finally blocks in the iterator body will have been executed by the exception propagation.
o The state of the enumerator object is changed to after.
o The exception propagation continues to the caller of the MoveNext method.
22.2.2 The Current property
An enumerator object’s Current property is affected by yield return statements in the iterator block.
When an enumerator object is in the suspended state, the value of Current is the value set by the previous call to MoveNext. When an enumerator object is in the before, running, or after states, the result of accessing Current is unspecified.
For an iterator block with a yield type other than object, the result of accessing Current through the enumerator object’s IEnumerable implementation corresponds to accessing Current through the enumerator object’s IEnumerator<T> implementation and casting the result to object.
22.2.3 The Dispose method
The Dispose method is used to clean up the iteration by bringing the enumerator object to the after state.
· If the state of the enumerator object is before, invoking Dispose changes the state to after.
· If the state of the enumerator object is running, the result of invoking Dispose is unspecified.
· If the state of the enumerator object is suspended, invoking Dispose:
o Changes the state to running.
o Executes any finally blocks as if the last executed yield return statement were a yield break statement. If this causes an exception to be thrown and propagated out of the iterator body, the state of the enumerator object is set to after and the exception is propagated to the caller of the Dispose method.
o Changes the state to after.
· If the state of the enumerator object is after, invoking Dispose has no affect.
22.3 Enumerable objects
When a function member returning an enumerable interface type is implemented using an iterator block, invoking the function member does not immediately execute the code in the iterator block. Instead, an enumerable object is created and returned. The enumerable object’s GetEnumerator method returns an enumerator object that encapsulates the code specified in the iterator block, and execution of the code in the iterator block occurs when the enumerator object’s MoveNext method is invoked. An enumerable object has the following characteristics:
· It implements IEnumerable and IEnumerable<T>, where T is the yield type of the iterator block.
· It is initialized with a copy of the argument values (if any) and instance value passed to the function member.
An enumerable object is typically an instance of a compiler-generated enumerable class that encapsulates the code in the iterator block and implements the enumerable interfaces, but other methods of implementation are possible. If an enumerable class is generated by the compiler, that class will be nested, directly or indirectly, in the class containing the function member, it will have private accessibility, and it will have a name reserved for compiler use (§2.4.2).
An enumerable object may implement more interfaces than those specified above. In particular, an enumerable object may also implement IEnumerator and IEnumerator<T>, enabling it to serve as both an enumerable and an enumerator. In that type of implementation, the first time an enumerable object’s GetEnumerator method is invoked, the enumerable object itself is returned. Subsequent invocations of the enumerable object’s GetEnumerator, if any, return a copy of the enumerable object. Thus, each returned enumerator has its own state and changes in one enumerator will not affect another.
22.3.1 The GetEnumerator method
An enumerable object provides an implementation of the GetEnumerator methods of the IEnumerable and IEnumerable<T> interfaces. The two GetEnumerator methods share a common implementation that acquires and returns an available enumerator object. The enumerator object is initialized with the argument values and instance value saved when the enumerable object was initialized, but otherwise the enumerator object functions as described in §22.2.
22.4 The yield statement
The yield statement is used in an iterator block to yield a value to the enumerator object or to signal the end of the iteration.
embedded-statement:
...
yield-statement
yield-statement:
yield return expression ;
yield break ;
To ensure compatibility with existing programs, yield is not a reserved word, and yield has special meaning only when it is used immediately before a return or break keyword. In other contexts, yield can be used as an identifier.
The are several restrictions on where a yield statement can appear, as described in the following.
· It is a compile-time error for a yield statement (of either form) to appear outside a method-body, operator-body or accessor-body
· It is a compile-time error for a yield statement (of either form) to appear inside an anonymous method.
· It is a compile-time error for a yield statement (of either form) to appear in the finally clause of a try statement.
· It is a compile-time error for a yield return statement to appear anywhere in a try statement that contains catch clauses.
The following example shows some valid and invalid uses of yield statements.
IEnumerator<int> GetEnumerator() {
try {
yield return 1; // Ok
yield break; // Ok
}
finally {
yield return 2; // Error, yield in finally
yield break; // Error, yield in finally
}
try {
yield return 3; // Error, yield return in try...catch
yield break; // Ok
}
catch {
yield return 4; // Error, yield return in try...catch
yield break; // Ok
}
D d = delegate {
yield return 5; // Error, yield in an anonymous method
};
}
int MyMethod() {
yield return 1; // Error, wrong return type for an iterator block
}
An implicit conversion (§6.1) must exist from the type of the expression in the yield return statement to the yield type (§22.1.3) of the iterator block.
A yield return statement is executed as follows:
· The expression given in the statement is evaluated, implicitly converted to the yield type, and assigned to the Current property of the enumerator object.
· Execution of the iterator block is suspended. If the yield return statement is within one or more try blocks, the associated finally blocks are not executed at this time.
· The MoveNext method of the enumerator object returns true to its caller, indicating that the enumerator object successfully advanced to the next item.
The next call to the enumerator object’s MoveNext method resumes execution of the iterator block from where it was last suspended.
A yield break statement is executed as follows:
· If the yield break statement is enclosed by one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all enclosing try statements have been executed.
· Control is returned to the caller of the iterator block. This is either the MoveNext method or Dispose method of the enumerator object.
Because a yield break statement unconditionally transfers control elsewhere, the end point of a yield break statement is never reachable.
22.4.1 Definite assignment
For a yield return statement stmt of the form:
yield return expr ;
· A variable v has the same definite assignment state at the beginning of expr as at the beginning of stmt.
· If a variable v is definitely assigned at the end of expr, it is definitely assigned at the end point of stmt; otherwise, it is not definitely assigned at the end point of stmt.
22.5 Implementation example
This section describes a possible implementation of iterators in terms of standard C# constructs. The implementation described here is based on the same principles used by the Microsoft C# compiler, but it is by no means a mandated implementation or the only one possible.
The following Stack<T> class implements its GetEnumerator method using an iterator. The iterator enumerates the elements of the stack in top to bottom order.
using System;
using System.Collections;
using System.Collections.Generic;
class Stack<T>: IEnumerable<T>
{
T[] items;
int count;
public void Push(T item) {
if (items == null) {
items = new T[4];
}
else if (items.Length == count) {
T[] newItems = new T[count * 2];
Array.Copy(items, 0, newItems, 0, count);
items = newItems;
}
items[count++] = item;
}
public T Pop() {
T result = items[--count];
items[count] = default(T);
return result;
}
public IEnumerator<T> GetEnumerator() {
for (int i = count - 1; i >= 0; --i) yield return items[i];
}
}
The GetEnumerator method can be translated into an instantiation of a compiler-generated enumerator class that encapsulates the code in the iterator block, as shown in the following.
class Stack<T>: IEnumerable<T>
{
...
public IEnumerator<T> GetEnumerator() {
return new __Enumerator1(this);
}
class __Enumerator1: IEnumerator<T>, IEnumerator
{
int __state;
T __current;
Stack<T> __this;
int i;
public __Enumerator1(Stack<T> __this) {
this.__this = __this;
}
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
switch (__state) {
case 1: goto __state1;
case 2: goto __state2;
}
i = __this.count - 1;
__loop:
if (i < 0) goto __state2;
__current = __this.items[i];
__state = 1;
return true;
__state1:
--i;
goto __loop;
__state2:
__state = 2;
return false;
}
public void Dispose() {
__state = 2;
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
In the preceding translation, the code in the iterator block is turned into a state machine and placed in the MoveNext method of the enumerator class. Furthermore, the local variable i is turned into a field in the enumerator object so it can continue to exist across invocations of MoveNext.
The following example prints a simple multiplication table of the integers 1 through 10. The FromTo method in the example returns an enumerable object and is implemented using an iterator.
using System;
using System.Collections.Generic;
class Test
{
static IEnumerable<int> FromTo(int from, int to) {
while (from <= to) yield return from++;
}
static void
IEnumerable<int> e = FromTo(1, 10);
foreach (int x in e) {
foreach (int y in e) {
Console.Write("{0,3} ", x * y);
}
Console.WriteLine();
}
}
}
The FromTo method can be translated into an instantiation of a compiler-generated enumerable class that encapsulates the code in the iterator block, as shown in the following.
using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;
class Test
{
...
static IEnumerable<int> FromTo(int from, int to) {
return new __Enumerable1(from, to);
}
class __Enumerable1:
IEnumerable<int>, IEnumerable,
IEnumerator<int>, IEnumerator
{
int __state;
int __current;
int __from;
int from;
int to;
int i;
public __Enumerable1(int __from, int to) {
this.__from = __from;
this.to = to;
}
public IEnumerator<int> GetEnumerator() {
__Enumerable1 result = this;
if (Interlocked.CompareExchange(ref __state, 1, 0) != 0) {
result = new __Enumerable1(__from, to);
result.__state = 1;
}
result.from = result.__from;
return result;
}
IEnumerator IEnumerable.GetEnumerator() {
return (IEnumerator)GetEnumerator();
}
public int Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
switch (__state) {
case 1:
if (from > to) goto case 2;
__current = from++;
__state = 1;
return true;
case 2:
__state = 2;
return false;
default:
throw new InvalidOperationException();
}
}
public void Dispose() {
__state = 2;
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
The enumerable class implements both the enumerable interfaces and the enumerator interfaces, enabling it to serve as both an enumerable and an enumerator. The first time the GetEnumerator method is invoked, the enumerable object itself is returned. Subsequent invocations of the enumerable object’s GetEnumerator, if any, return a copy of the enumerable object. Thus, each returned enumerator has its own state and changes in one enumerator will not affect another. The Interlocked.CompareExchange method is used to ensure thread-safe operation.
The from and to parameters are turned into fields in the enumerable class. Because from is modified in the iterator block, an additional __from field is introduced to hold the initial value given to from in each enumerator.
The MoveNext method throws an InvalidOperationException if it is called when __state is 0. This protects against use of the enumerable object as an enumerator object without first calling GetEnumerator.
The following example shows a simple tree class. The Tree<T> class implements its GetEnumerator method using an iterator. The iterator enumerates the elements of the tree in infix order.
using System;
using System.Collections.Generic;
class Tree<T>: IEnumerable<T>
{
T value;
Tree<T> left;
Tree<T> right;
public Tree(T value, Tree<T> left, Tree<T> right) {
this.value = value;
this.left = left;
this.right = right;
}
public IEnumerator<T> GetEnumerator() {
if (left != null) foreach (T x in left) yield x;
yield value;
if (right != null) foreach (T x in right) yield x;
}
}
class Program
{
static Tree<T> MakeTree<T>(T[] items, int left, int right) {
if (left > right) return null;
int i = (left + right) / 2;
return new Tree<T>(items[i],
MakeTree(items, left, i - 1),
MakeTree(items, i + 1, right));
}
static Tree<T> MakeTree<T>(params T[] items) {
return MakeTree(items, 0, items.Length - 1);
}
// The output of the program is:
// 1 2 3 4 5 6 7 8 9
// Mon Tue Wed Thu Fri Sat Sun
static void
Tree<int> ints = MakeTree(1, 2, 3, 4, 5, 6, 7, 8, 9);
foreach (int i in ints) Console.Write("{0} ", i);
Console.WriteLine();
Tree<string> strings = MakeTree(
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun");
foreach (string s in strings) Console.Write("{0} ", s);
Console.WriteLine();
}
}
The GetEnumerator method can be translated into an instantiation of a compiler-generated enumerator class that encapsulates the code in the iterator block, as shown in the following.
class Tree<T>: IEnumerable<T>
{
...
public IEnumerator<T> GetEnumerator() {
return new __Enumerator1(this);
}
class __Enumerator1 : IEnumerator<T>, IEnumerator
{
Node<T> __this;
IEnumerator<T> __left, __right;
int __state;
T __current;
public __Enumerator1(Node<T> __this) {
this.__this = __this;
}
public T Current {
get { return __current; }
}
object IEnumerator.Current {
get { return __current; }
}
public bool MoveNext() {
try {
switch (__state) {
case 0:
__state = -1;
if (__this.left == null) goto __yield_value;
__left = __this.left.GetEnumerator();
goto case 1;
case 1:
__state = -2;
if (!__left.MoveNext()) goto __left_dispose;
__current = __left.Current;
__state = 1;
return true;
__left_dispose:
__state = -1;
__left.Dispose();
__yield_value:
__current = __this.value;
__state = 2;
return true;
case 2:
__state = -1;
if (__this.right == null) goto __end;
__right = __this.right.GetEnumerator();
goto case 3;
case 3:
__state = -3;
if (!__right.MoveNext()) goto __right_dispose;
__current = __right.Current;
__state = 3;
return true;
__right_dispose:
__state = -1;
__right.Dispose();
__end:
__state = 4;
break;
}
}
finally {
if (__state < 0) Dispose();
}
return false;
}
public void Dispose() {
try {
switch (__state) {
case 1:
case -2:
__left.Dispose();
break;
case 3:
case -3:
__right.Dispose();
break;
}
}
finally {
__state = 4;
}
}
void IEnumerator.Reset() {
throw new NotSupportedException();
}
}
}
The compiler generated temporaries used in the foreach statements are lifted into the __left and __right fields of the enumerator object. The __state field of the enumerator object is carefully updated so that the correct Dispose() method will be called correctly if an exception is thrown. Note that it is not possible to write the translated code with simple foreach statements.
23. Partial types
23.1 Partial declarations
A new type modifier, partial, is used when defining a type in multiple parts. To ensure compatibility with existing programs, this modifier is different than other modifiers: like get and set, it is not a keyword, and it must appear immediately before one of the keywords class, struct, or interface.
class-declaration:
attributesopt class-modifiersopt partialopt class identifier type-parameter-listopt
class-baseopt type-parameter-constraints-clausesopt class-body ;opt
struct-declaration:
attributesopt struct-modifiersopt partialopt struct identifier type-parameter-listopt
struct-interfacesopt type-parameter-constraints-clausesopt struct-body ;opt
interface-declaration:
attributesopt interface-modifiersopt partialopt interface identifier type-parameter-listopt
interface-baseopt type-parameter-constraints-clausesopt interface-body ;opt
Each part of a partial type declaration must include a partial modifier and must be declared in the same namespace as the other parts. The partial modifier indicates that additional parts of the type declaration may exist elsewhere, but the existence of such additional parts is not a requirement; it is valid for just a single declaration of a type to include the partial modifier.
All parts of a partial type must be compiled together such that the parts can be merged at compile-time. Partial types specifically do not allow already compiled types to be extended.
Nested types may be declared in multiple parts by using the partial modifier. Typically, the containing type is declared using partial as well, and each part of the nested type is declared in a different part of the containing type.
The partial modifier is not permitted on delegate or enum declarations.
23.1.1 Attributes
The attributes of a partial type are determined by combining, in an unspecified order, the attributes of each of the parts. If an attribute is placed on multiple parts, it is equivalent to specifying the attribute multiple times on the type. For example, the two parts:
[Attr1, Attr2("hello")]
partial class A {}
[Attr3, Attr2("goodbye")]
partial class A {}
are equivalent to a declaration such as:
[Attr1, Attr2("hello"), Attr3, Attr2("goodbye")]
class A {}
Attributes on type parameters combine in a similar fashion.
23.1.2 Modifiers
When a partial type declaration includes an accessibility specification (the public, protected, internal, and private modifiers) it must agree with all other parts that include an accessibility specification. If no part of a partial type includes an accessibility specification, the type is given the appropriate default accessibility (§3.5.1).
If one or more partial declarations of a nested type include a new modifier, no warning is reported if the nested type hides an inherited member (§3.7.1.2).
If one or more partial declarations of a class include an abstract modifier, the class is considered abstract (§10.1.1.1). Otherwise, the class is considered non-abstract.
If one or more partial declarations of a class include a sealed modifier, the class is considered sealed (§10.1.1.2). Otherwise, the class is considered unsealed.
Note that a class cannot be both abstract and sealed.
When the unsafe modifier is used on a partial type declaration, only that particular part is considered an unsafe context (§18.1).
23.1.3 Type parameters and constraints
If a generic type is declared in multiple parts, each part must state the type parameters. Each part must have the same number of type parameters, and the same name for each type parameter, in order.
When a partial generic type declaration includes constraints (where clauses), the constraints must agree with all other parts that include constraints. Specifically, each part that includes constraints must have constraints for the same set of type parameters, and for each type parameter the sets of primary, secondary, and constructor constraints must be equivalent. Two sets of constraints are equivalent if they contain the same members. If no part of a partial generic type specifies type parameter constraints, the type parameters are considered unconstrained.
The example
partial class Dictionary<K,V>
where K: IComparable<K>
where V: IKeyProvider<K>, IPersistable
{
...
}
partial class Dictionary<K,V>
where V: IPersistable, IKeyProvider<K>
where K: IComparable<K>
{
...
}
partial class Dictionary<K,V>
{
...
}
is correct because those parts that include constrains (the first two) effectively specify the same set of primary, secondary, and constructor constraints for the same set of type parameters, respectively.
23.1.4 Base class
When a partial class declaration includes a base class specification it must agree with all other parts that include a base class specification. If no part of a partial class includes a base class specification, the base class becomes System.Object (§10.1.2.1).
23.1.5 Base interfaces
The set of base interfaces for a type declared in multiple parts is the union of the base interfaces specified on each part. A particular base interface may only be named once on each part, but it is permitted for multiple parts to name the same base interface(s). There must only be one implementation of the members of any given base interface.
In the example
partial class C: IA, IB {...}
partial class C: IC {...}
partial class C: IA, IB {...}
the set of base interfaces for class C is IA, IB, and IC.
Typically, each part provides an implementation of the interface(s) declared on that part; however, this is not a requirement. A part may provide the implementation for an interface declared on a different part:
partial class X
{
int IComparable.CompareTo(object o) {...}
}
partial class X: IComparable
{
...
}
23.1.6 Members
The members of a type declared in multiple parts is simply the union of the members declared in each part. The bodies of all parts of the type declaration share the same declaration space (§3.3), and the scope of each member (§3.7) extends to the bodies of all the parts. The accessibility domain of any member always includes all the parts of the enclosing type; a private member declared in one part is freely accessible from another part. It is a compile-time error to declare the same member in more than one part of the type, unless that member is a type with the partial modifier.
partial class A
{
int x; // Error, cannot declare x more than once
partial class
{
int y;
}
}
partial class A
{
int x; // Error, cannot declare x more than once
partial class Inner // Ok,
{
int z;
}
}
Although the ordering of members within a type is not significant to C# code, it may be significant when interfacing with other languages and environments. In these cases, the ordering of members within a type declared in multiple parts is undefined.
23.2 Nam e binding
Although each part of an extensible type must be declared within the same namespace, the parts are typically written within different namespace declarations. Thus, different using directives (§9.3) may be present for each part. When interpreting simple names (§7.5.2) within one part, only the using directives of the namespace declaration(s) enclosing that part are considered. This may result in the same identifier having different meanings in different parts:
namespace N
{
using List = System.Collections.ArrayList;
partial class A
{
List x; // x has type System.Collections.ArrayList
}
}
namespace N
{
using List = Widgets.LinkedList;
partial class A
{
List y; // y has type Widgets.LinkedList
}
}
24. Nullable types
24.1 Nullable types
A nullable type is classified as a value type:
value-type:
struct-type
enum-type
struct-type:
type-name
simple-type
nullable-type
nullable-type:
non-nullable-value-type ?
non-nullable-value-type:
type
The type specified before the ? modifier in a nullable type is called the underlying type of the nullable type. The underlying type of a nullable type can be any non-nullable value type or any type parameter that is constrained to non-nullable value types (that is, any type parameter with a struct constraint). The underlying type of a nullable type cannot be a nullable type or a reference type. For example, int?? and string? are invalid types.
A nullable type can represent all values of its underlying type plus an additional null value.
The syntax T? is shorthand for System.Nullable<T>, and the two forms can be used interchangeably.
24.1.1 Members
An instance of a nullable type T? has two public read-only properties:
· A HasValue property of type bool
· A Value property of type T
An instance for which HasValue is true is said to be non-null. A non-null instance contains a known value and Value returns that value.
An instance for which HasValue is false is said to be null. A null instance has an undefined value. Attempting to read the Value of a null instance causes a System.InvalidOperationException to be thrown.
In addition to the default constructor, every nullable type T? has a public constructor that takes a single argument of type T. Given a value x of type T, a constructor invocation of the form
new T?(x)
creates a non-null instance of T? for which the Value property is x.
It is never necessary to explicitly invoke a nullable type’s constructor since equivalent functionality is provided as an implicit conversion from T to T?.
24.1.2 Default value
The default value of a nullable type is an instance for which the HasValue property is false and the Value property is undefined. The default value is also known as the null value of the nullable type. An implicit conversion exists from the null literal to any nullable type, and this conversion produces the null value of the type.
24.1.3 The value type constraint
The value type constraint, written as the keyword struct, constrains a type parameter to non-nullable value types (§20.7). Specifically, when a type parameter is declared with the struct constraint, a type argument used for that type parameter can be any value type except a nullable type.
The System.Nullable<T> type specifies the value type constraint for T. Thus, recursively constructed types of the forms T?? and Nullable<Nullable<T>> are prohibited.
24.2 Conversions
The following terms are used in the subsequent sections:
· The term wrapping denotes the process of packaging a value, of type T, in an instance of type T?. A value x of type T is wrapped to type T? by evaluating the expression new T?(x).
· The term unwrapping denotes the process of obtaining the value, of type T, contained in an instance of type T?. A value x of type T? is unwrapped to type T by evaluating the expression x.Value. Attempting to unwrap a null instance causes a System.InvalidOperationException to be thrown.
24.2.1 Null literal conversions
An implicit conversion exists from the null literal to any nullable type. This conversion produces the null value (§24.1.2) of the given nullable type.
24.2.2 Nullable conversions
Nullable conversions permit predefined conversions that operate on non-nullable value types to also be used with nullable forms of those types. For each of the predefined implicit or explicit conversions that convert from a non-nullable value type S to a non-nullable value type T (§6.1.1, §6.1.2, §6.1.3, §6.1.6, §6.2.1, and §6.2.2), the following nullable conversions exist:
· An implicit or explicit conversion from S? to T?.
· An implicit or explicit conversion from S to T?.
· An explicit conversion from S? to T.
A nullable conversion is itself classified as an implicit or explicit conversion.
Certain nullable conversions are classified as standard conversions and can occur as part of a user-defined conversion. Specifically, all implicit nullable conversions are classified as standard implicit conversions (§6.3.1), and those explicit nullable conversions that satisfy the requirements of §6.3.2 are classified as standard explicit conversions.
Evaluation of a nullable conversion based on an underlying conversion from S to T proceeds as follows:
· If the nullable conversion is from S? to T?:
o If the source value is null (HasValue property is false), the result is the null value of type T?.
o Otherwise, the conversion is evaluated as an unwrapping from S? to S, followed by the underlying conversion from S to T, followed by a wrapping from T to T?.
· If the nullable conversion is from S to T?, the conversion is evaluated as the underlying conversion from S to T followed by a wrapping from T to T?.
· If the nullable conversion is from S? to T, the conversion is evaluated as an unwrapping from S? to S followed by the underlying conversion from S to T.
24.2.3 Boxing and unboxing conversions
Nullable types behave differently than other value types in boxing and unboxing conversions. When a value of a nullable type is boxed, the result is either a null reference or a reference to a boxed value of the underlying type. Conversely, a null reference or a reference to a boxed value of a given type can be unboxed as a value of the nullable form of that type. For example, when an int? is boxed, the result is either a null reference or a reference to a boxed int, and a null reference or a reference to a boxed int can be unboxed as an int?.
A nullable type T? has boxing conversions (§6.1.5) to the same set of types as T. A boxing conversion from a nullable type T? is processed as follows:
· If the source value is null (HasValue property is false), the result is a null reference of the target type.
· Otherwise, the result is a reference to a boxed T produced by unwrapping and boxing the source value.
A nullable type T? has unboxing conversions (§6.2.4) from the same set of types as T. An unboxing conversion to a nullable type T? is processed as follows:
· If the source is a null reference, the result is a null value of type T?.
· If the source is a reference to a boxed T, the result is a T? produced by unboxing and wrapping the source.
· Otherwise, a System.InvalidCastException is thrown.
The rules above effectively unify the null representations of nullable types and reference types—a null value of a nullable type becomes a null reference when boxed, and a null reference becomes a null value of a nullable type when unboxed. The rules furthermore guarantee that the run-time type of a boxed value is always a non-nullable value type and never a nullable type.
Since a nullable type T? has boxing conversions to the same set of types as T, a boxing conversion exists from T? to any interface implemented by T, and an unboxing conversion exists from any interface implemented by T to T?. However, a nullable type never satisfies an interface constraint, even if the underlying type implements the particular interface.
The example below shows several boxing and unboxing operations involving nullable types.
int i = 123;
int? x = 456;
int? y = null;
object o1 = i; // o1 = reference to boxed int 123
object o2 = x; // o2 = reference to boxed int 456
object o3 = y; // o3 = null
int i1 = (int)o1; // i1 = 123
int i2 = (int)o2; // i2 = 456
int i3 = (int)o3; // Throws System.NullReferenceException
int? ni1 = (int?)o1; // ni1 = 123
int? ni2 = (int?)o2; // ni2 = 456
int? ni3 = (int?)o3; // ni3 = null
24.2.4 Permitted user-defined conversions
The rules governing permitted user-defined conversion operator declarations are modified to allow a struct to also declare conversion operators that convert to or from nullable forms of the struct type. The following replaces the restrictions listed in §6.4.1 and §10.9.3.
A class or struct is permitted to declare a conversion from a source type S to a target type T only if all of the following are true, where S0 and T0 are the types that result from removing the trailing ? modifiers, if any, from S and T:
· S0 and T0 are different types.
· Either S0 or T0 is the class or struct type in which the operator declaration takes place.
· Neither S0 nor T0 is an interface-type.
· Excluding user-defined conversions, a conversion does not exist from S to T or from T to S.
24.2.5 Evaluation of user-defined conversions
Evaluation of user-defined conversions (§6.4.2) is modified as follows to support nullable types:
· Trailing ? modifiers, if any, are removed from the source and target types before determining the set of types from which user-defined conversion operators will be considered. For example, when converting from a type S? to a type T?, the set of types from which user-defined conversion operators will be considered consists of S and T.
· When the source and target types are both nullable, the set of applicable conversion operators includes not just user-defined conversion operators but also lifted conversion operators (§24.2.6).
· When determining the most specific conversion operator, user-defined conversion operators are preferred over lifted conversion operators.
24.2.6 Lifted conversions
Given a user-defined conversion operator that converts from a non-nullable value type S to a non-nullable value type T, a lifted conversion operator exists that converts from S? to T?. This lifted conversion operator performs an unwrapping from S? to S followed by the user-defined conversion from S to T followed by a wrapping from T to T?, except that a null valued S? converts directly to a null valued T?.
A lifted conversion operator has the same implicit or explicit classification as its underlying user-defined conversion operator.
24.2.7 User-defined implicit conversions
The following replaces §6.4.3.
A user-defined implicit conversion from type S to type T is processed as follows:
· Determine the types S0 and T0 that result from removing the trailing ? modifiers, if any, from S and T.
· Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of S0 (if S0 is a class or struct), the base classes of S0 (if S0 is a class), and T0 (if T0 is a class or struct).
· Find the set of applicable conversion operators, U. This set consists of the user-defined and, if S and T are both nullable, lifted (§24.2.6) implicit conversion operators declared by the classes or structs in D that convert from a type encompassing S to a type encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.
· Find the most specific source type, SX, of the operators in U:
o If any of the operators in U convert from S, then SX is S.
o Otherwise, SX is the most encompassed type in the combined set of source types of the operators in U. If exactly one most encompassed type cannot be found, then the conversion is ambiguous and a compile-time error occurs.
· Find the most specific target type, TX, of the operators in U:
o If any of the operators in U convert to T, then TX is T.
o Otherwise, TX is the most encompassing type in the combined set of target types of the operators in U. If exactly one most encompassing type cannot be found, then the conversion is ambiguous and a compile-time error occurs.
· Find the most specific conversion operator:
o If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator.
o Otherwise, if U contains exactly one lifted conversion operator that converts from SX to TX, then this is the most specific conversion operator.
o Otherwise, the conversion is ambiguous and a compile-time error occurs.
· Finally, apply the conversion:
o If S is not SX, then a standard implicit conversion from S to SX is performed.
o The most specific conversion operator is invoked to convert from SX to TX.
o If TX is not T, then a standard implicit conversion from TX to T is performed.
24.2.8 User-defined explicit conversions
The following replaces §6.4.4.
A user-defined explicit conversion from type S to type T is processed as follows:
· Determine the types S0 and T0 that result from removing the trailing ? modifiers, if any, from S and T.
· Find the set of types, D, from which user-defined conversion operators will be considered. This set consists of S0 (if S0 is a class or struct), the base classes of S0 (if S0 is a class), T0 (if T0 is a class or struct), and the base classes of T0 (if T0 is a class).
· Find the set of applicable conversion operators, U. This set consists of the user-defined and, if S and T are both nullable, lifted (§24.2.6) implicit or explicit conversion operators declared by the classes or structs in D that convert from a type encompassing or encompassed by S to a type encompassing or encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.
· Find the most specific source type, SX, of the operators in U:
o If any of the operators in U convert from S, then SX is S.
o Otherwise, if any of the operators in U convert from types that encompass S, then SX is the most encompassed type in the combined set of source types of those operators. If exactly one most encompassed type cannot be found, then the conversion is ambiguous and a compile-time error occurs.
o Otherwise, SX is the most encompassing type in the combined set of source types of the operators in U. If exactly one most encompassing type cannot be found, then the conversion is ambiguous and a compile-time error occurs.
· Find the most specific target type, TX, of the operators in U:
o If any of the operators in U convert to T, then TX is T.
o Otherwise, if any of the operators in U convert to types that are encompassed by T, then TX is the most encompassing type in the combined set of target types of those operators. If exactly one most encompassing type cannot be found, then the conversion is ambiguous and a compile-time error occurs.
o Otherwise, TX is the most encompassed type in the combined set of target types of the operators in U. If exactly one most encompassed type cannot be found, then the conversion is ambiguous and a compile-time error occurs.
· Find the most specific conversion operator:
o If U contains exactly one user-defined conversion operator that converts from SX to TX, then this is the most specific conversion operator.
o Otherwise, if U contains exactly one lifted conversion operator that converts from SX to TX, then this is the most specific conversion operator.
o Otherwise, the conversion is ambiguous and a compile-time error occurs.
· Finally, apply the conversion:
o If S is not SX, then a standard explicit conversion from S to SX is performed.
o The most specific conversion operator is invoked to convert from SX to TX.
o If TX is not T, then a standard explicit conversion from TX to T is performed.
24.3 Expressions
24.3.1 Lifted operators
Lifted operators permit predefined and user-defined operators that operate on non-nullable value types to also be used with nullable forms of those types. Lifted operators are constructed from predefined and user-defined operators that meet certain requirements, as described in the following:
· For the unary operators
+ ++ - -- ! ~
a lifted form of an operator exists if the operand and result types are both non-nullable value types. The lifted form is constructed by adding a single ? modifier to the operand and result types. The lifted operator produces a null value if the operand is null. Otherwise, the lifted operator unwraps the operand, applies the underlying operator, and wraps the result.
· For the binary operators
+ - * / % & | ^ << >>
a lifted form of an operator exists if the operand and result types are all non-nullable value types. The lifted form is constructed by adding a single ? modifier to each operand and result type. The lifted operator produces a null value if one or both operands are null (an exception being the & and | operators of the bool? type, as described in §24.3.8). Otherwise, the lifted operator unwraps the operands, applies the underlying operator, and wraps the result.
· For the equality operators
== !=
a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator considers two null values equal, and a null value unequal to any non-null value. If both operands are non-null, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.
· For the relational operators
< > <= >=
a lifted form of an operator exists if the operand types are both non-nullable value types and if the result type is bool. The lifted form is constructed by adding a single ? modifier to each operand type. The lifted operator produces the value false if one or both operands are null. Otherwise, the lifted operator unwraps the operands and applies the underlying operator to produce the bool result.
24.3.2 Permitted user-defined operators
The rules governing permitted user-defined operator declarations are modified to allow a struct to also declare operators for nullable forms of the struct type. The following replaces the restrictions listed in §10.9.1 and §10.9.2, where T denotes the class or struct type that contains the operator declaration:
· A unary +, -, !, or ~ operator must take a single parameter of type T or T? and can return any type.
· A unary ++ or -- operator must take a single parameter of type T or T? and must return that same type.
· A unary true or false operator must take a single parameter of type T or T? and must return type bool.
· A binary non-shift operator must take two parameters, at least one of which must have type T or T?, and can return any type.
· A binary << or >> operator must take two parameters, the first of which must have type T or T? and the second of which must have type int or int?, and can return any type.
Because of the rules above, it is possible for an unlifted operator and a lifted operator to have the same signature. In that case, preference is given to the unlifted operator, as described in 24.3.3.
24.3.3 Operator overload resolution
The rules for unary and binary operator overload resolution (§7.2.3 and §7.2.4) are modified as follows to support lifted operators:
· Trailing ? modifiers, if any, are removed from the operand types to determine the types in which to locate user-defined operator declarations. For example, if the operands are of type X? and Y?, the set of candidate operators is determined by examining X and Y.
· When determining the set of candidate user-defined operators (§7.2.5), lifted forms of the operators declared in a type are considered to also be declared by the type.
· Operator lifting applies to predefined operators, and the lifted forms of the predefined operators are themselves considered predefined operators.
· When selecting the single best operator, if two operators have identical signatures, an unlifted operator is better than a lifted operator.
24.3.4 Equality operators and null
The == and != operators permit one operand to be a value of a nullable type and the other to be the null literal, even if no predefined or user-defined operator (in unlifted or lifted form) exists for the operation.
For an operation of one of the forms
x == null null == x x != null null != x
where x is an expression of a nullable type, if operator overload resolution (§7.2.4 and §24.3.3) fails to find an applicable operator, the result is instead computed from the HasValue property of x. Specifically, the first two forms are translated into !x.HasValue, and last two forms are translated into x.HasValue.
24.3.5 The is operator
The is operator (§7.9.9) is extended to support nullable types. An operation of the form e is T, where e is an expression and T is a type, is evaluated as follows, after type arguments have been substituted for all type parameters:
· If the type of e is a reference type or a nullable type and the value of e is null, the result is false.
· Otherwise, let D represent the dynamic type of e as follows:
o If the type of e is a reference type, D is the run-time type of the instance reference by e.
o If the type of e is a nullable type, D is the underlying type of that nullable type.
o If the type of e is a non-nullable value type, D is the type of e.
· The result of the operation depends on D and T as follows:
o If T is a reference type, the result is true if D and T are the same type, if D is a reference type and an implicit reference conversion from D to T exists, or if D is a value type and a boxing conversion from D to T exists.
o If T is a nullable type, the result is true if D is the underlying type of T.
o If T is a non-nullable value type, the result is true if D and T are the same type.
o Otherwise, the result is false.
24.3.6 The as operator
The as operator (§7.9.10) is extended to support nullable types. In an operation of the form e as T, e must be an expression and T must be a reference type, a type parameter known to be a reference type, or a nullable type. Furthermore, at least one of the following must be true, or otherwise a compile-time error occurs:
· An identity (§6.1.1), implicit reference (§6.1.4), boxing (§6.1.5, §24.2.3), explicit reference (§6.2.3), or unboxing (§6.2.4, §24.2.3) conversion exists from the type of e to T.
· The type of e or T is an open type.
· e is the null literal.
The operation e as T produces the same result as
e is T ? (T)(e) : (T)null
except that e is only evaluated once. The compiler can be expected to optimize e as T to perform at most one dynamic type check as opposed to the two dynamic type checks implied by the expansion above.
24.3.7 Compound assignment
Compound assignment operations (§7.13.2) support lifted operators. Since a compound assignment x op= y is evaluated as either x = x op y or x = (T)(x op y), the existing rules of evaluation implicitly cover lifted operators and no changes to the rules are required.
24.3.8 The bool? type
The nullable boolean type bool? can represent three values, true, false, and null, and is conceptually similar to the three-valued type used for boolean expressions in SQL. To ensure that the results produced by the & and | operators for bool? operands are consistent with SQL’s three-valued logic, the following predefined operators are provided:
bool? operator &(bool? x, bool? y);
bool? operator |(bool? x, bool? y);
The following table lists the results produced by these operators for all combinations of the values true, false, and null.
x |
y |
x & y |
x | y |
true |
true |
true |
true |
true |
false |
false |
true |
true |
null |
null |
true |
false |
true |
false |
true |
false |
false |
false |
false |
false |
null |
false |
null |
null |
true |
null |
true |
null |
false |
false |
null |
null |
null |
null |
null |
24.3.9 The null coalescing operator
The ?? operator is called the null coalescing operator.
null-coalescing-expression:
conditional-or-expression
conditional-or-expression ?? null-coalescing-expression
A new ?? token is added to the C# lexical grammar. The updated form of the operator-or-punctuator lexical grammar production is shown in §20.10.
A null coalescing expression of the form a ?? b requires a to be of a nullable type or reference type. If a is non-null, the result of a ?? b is a; otherwise, the result is b. The operation evaluates b only if a is null.
The null coalescing operator is right-associative, meaning that operations are grouped from right to left. For example, an expression of the form a ?? b ?? c is evaluated as a ?? (b ?? c). In general terms, an expression of the form E1 ?? E2 ?? ... ?? EN returns the first of the operands that is non-null, or null if all operands are null.
The type of the expression a ?? b depends on which implicit conversions are available between the types of the operands. In order of preference, the type of a ?? b is A0, A, or B, where A is the type of a, B is the type of b, and A0 is the type that results from removing the trailing ? modifier, if any, from A. Specifically, a ?? b is processed as follows:
· If A is not a nullable type or a reference type, a compile-time error occurs.
· If A is a nullable type and an implicit conversion exists from b to A0, the result type is A0. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0, and this becomes the result. Otherwise, b is evaluated and converted to type A0, and this becomes the result.
· Otherwise, if an implicit conversion exists from b to A, the result type is A. At run-time, a is first evaluated. If a is not null, a becomes the result. Otherwise, b is evaluated and converted to type A, and this becomes the result.
· Otherwise, if an implicit conversion exists from A0 to B, the result type is B. At run-time, a is first evaluated. If a is not null, a is unwrapped to type A0 (unless A and A0 are the same type) and converted to type B, and this becomes the result. Otherwise, b is evaluated and becomes the result.
· Otherwise, a and b are incompatible, and a compile-time error occurs.
25. Other features
25.1 Property accessor accessibility
Occasionally, a component design will
25.1.1 Accessor declarations
The syntax for property accessors and indexer accessors is modified to permit an optional accessor-modifier:
get-accessor-declaration:
attributesopt accessor-modifieropt get accessor-body
set-accessor-declaration:
attributesopt accessor-modifieropt set accessor-body
accessor-modifier:
protected
internal
private
protected internal
internal protected
The use of accessor-modifiers is governed by the following restrictions:
· An accessor-modifier may not be used in an interface or in an explicit interface member implementation.
· For a property or indexer that has no override modifer, an accessor-modifier is permitted only if the property or indexer has both a get and set accessor, and then is permitted only on one of those accessors.
· For a property or indexer that includes an override modifer, an accessor must match the accessor-modifier, if any, of the accessor being overridden.
· The accessor-modifier must declare an accessibility that is strictly more restrictive than the declared accessibility of the property or indexer itself. To be precise:
o If the property or indexer has a declared accessibility of public, any accessor-modifier may be used.
o If the property or indexer has a declared accessibility of protected internal, the accessor-modifier may be either internal, protected, or private.
o If the property or indexer has a declared accessibility of internal or protected, the accessor-modifier must be private.
o If the property or indexer has a declared accessibility of private, no accessor-modifier may be used.
If an accessor has an accessor-modifier, the accessibility domain (§3.5.2) of the accessor is determined using the declared accessibility of the accessor-modifier. If an accessor does not have an accessor-modifier, the accessibility domain of the accessor is determined from the declared accessibility of the property or
25.1.2 Accessor usa ge
The presence of an accessor-modifier
Once a particular property or indexer has been selected, the accessibility domains of the specific accessors involved are used to determine if that
· If the usage is as a value (§7.1.1), the get accessor must exist and be accessible.
· If the usage is as the target of a simple assignment (§7.13.1), the set accessor must exist and be accessible.
· If the usage is as the target of compound assignment (§7.13.2), or as the target of the ++ or -- operators (§7.5.9, §7.6.5), both the get accessors and the set accessor must exist and be accessible.
In the following example, the property A.Text is hidden by the property B.Text, even in contexts where only the set accessor is called. In contrast, the property B.Count is not accessible to class M, so the accessible property A.Count is used instead.
class A
{
public string Text {
get { return "hello"; }
set { }
}
public int Count {
get { return 5; }
set { }
}
}
class B: A
{
private string text = "goodbye";
private int count = 0;
new public string Text {
get { return text; }
protected set { text = value; }
}
new protected int Count {
get { return count; }
set { count = value; }
}
}
class M
{
static void
B b = new B();
b.Count = 12; // Calls A.Count set accessor
int i = b.Count; // Calls A.Count get accessor
b.Text = "howdy"; // Error, B.Text set accessor not accessible
string s = b.Text; // Calls B.Text get accessor
}
}
25.1.3 Overriding and interface implementation
When a property or indexer is declared as an override, any overridden accessors must be accessible to the overriding code. In addition, the declared accessibility of both the property or
public class B
{
public virtual int P {
protected set {...}
get {...}
}
}
public class D: B
{
public override int P {
protected set {...} // Must specify protected here
get {...} // Must not have a modifier here
}
}
An accessor that is used to implement an interface may not have an accessor-modifier. If only one accessor is used to implement an interface, the other accessor may be declared with an accessor-modifier:
public interface I
{
string Prop { get; }
}
public class C: I
{
public Prop {
get { return "April"; } // Must not have a modifier here
internal set {...} // Ok, because I.Prop has no set accessor
}
}
25.2 Static classes
Classes that are not intended to be instantiated and which contain only static members are commonly declared as sealed classes with a private constructor. Examples of such classes in the .NET Framework class library include System.Console and System.Environment. While the “sealed class with private constructor” design pattern prevents instantiation and subclassing of the given class, it doesn’t prevent the class from being used as the type of a variable or parameter and it doesn’t prevent the class from declaring instance members. This is unfortunate, since variables or parameters of the class type are close to meaningless (they can only be null) and instance members in the class are inaccessible (no instances exist through which the members can be accessed).
Static classes formalize the “sealed class with private constructor” design pattern and provide stronger checking of the restrictions that are logically associated with the pattern.
25.2.1 Static class declarations
When a class declaration includes a static modifier, the class being declared is said to be a static class.
class-declaration:
attributesopt class-modifiersopt partialopt class identifier type-parameter-listopt
class-baseopt type-parameter-constraints-clausesopt class-body ;opt
class-modifiers:
class-modifier
class-modifiers class-modifier
class-modifier:
new
public
protected
internal
private
abstract
sealed
static
A static class declaration is subject to the following restrictions:
· A static class may not include a sealed or abstract modifier. Note, however, that since a static class cannot be instantiated or derived from, it behaves as if it was both sealed and abstract.
· A static class may not include a class-base specification (§10.1.2) and cannot explicitly specify a base class or a list of implemented interfaces. A static class implicitly inherits from type object.
· A static class can only contain static members (§10.2.5). Note that constants and nested types are classified as static members.
· A static class cannot have members with protected or protected internal declared accessibility.
It is a compile-time error to violate any of these restrictions.
A static class has no instance constructors. It is not possible to declare an instance constructor in a static class, and no default instance constructor (§10.10.4) is provided for a static class.
The members of a static class are not automatically static, and the member declarations must explicitly include a static modifier (except for constants and nested types). When a class is nested within a static outer class, the nested class is not a static class unless it explicitly includes a static modifier.
25.2.2 Referencing static class types
A namespace-or-type-name (§20.9.1) is permitted to reference a static class if
· The namespace-or-type-name is the T in a namespace-or-type-name of the form T.I, or
· The namespace-or-type-name is the T in a typeof-expression (§7.5.11) of the form typeof(T).
A primary-expression (§7.5) is permitted to reference a static class if
· The primary-expression is the E in a member-access (§7.5.4) of the form E.I.
In any other context it is a compile-time error to reference a static class. For example, it is an error for a static class to be used as a base class, a constituent type (§10.2.4) of a member, a generic type argument, or a type parameter constraint. Likewise, a static class cannot be used in an array type, a pointer type, a new expression, a cast expression, an is expression, an as expression, a sizeof expression, or a default value expression.
25.3 Nam espace alias qualifiers
When types or namespaces are added to an assembly, the names of those types or namespaces may conflict with names that are already in use in dependent programs. For example, consider the following two assemblies:
Assembly a1.dll:
namespace System.IO
{
public class Stream {...}
public class FileStream: Stream {...}
...
}
Assembly a2.dll:
namespace MyLibrary.IO
{
public class EmptyStream: Stream {...}
}
and the following program that references the two assemblies:
using System.IO;
using MyLibrary.IO;
class Program
{
static void
Stream s = new EmptyStream();
...
}
}
If, at some future point, a type named System.IO.EmptyStream is added to a1.dll, then the reference to EmptyStream in the program above would become ambiguous, and a compile-time error would occur.
It is to some extent possible to guard against version related code
using SIO = System.IO;
using MIO = MyLibrary.IO;
class Program
{
static void
SIO.Stream s = new MIO.EmptyStream();
...
}
}
then the introduction of a type named System.IO.EmptyStream would not cause errors. However, even with this explicit qualification approach, there are situations where the introduction of new types or members can cause errors. For example, in the program above, an ambiguity error would occur if a referenced assembly introduced a top-level namespace named MIO.
The namespace alias qualifier :: makes it possible to guarantee that type name lookups are unaffected by the introduction of new types and members. The namespace alias qualifier always appears between two identifiers, referred to as the left-hand and right-hand identifiers. Unlike the regular . qualifier, the left-hand identifier of the :: qualifier is looked up only as an extern or using alias.
In the example
using SIO = System.IO;
using MIO = MyLibrary.IO;
class Program
{
static void
SIO::Stream s = new MIO::EmptyStream();
...
}
}
when the type names SIO::Stream and MIO::EmptyStream are resolved, SIO and MIO are looked up only as extern or using aliases (§25.4 and §9.3.1). Since the scope of an extern or using alias does not extend beyond the source file in which the alias is defined, it is not possible for a referenced assembly to introduce any entities that would affect the resolution. Thus, by defining using aliases for all referenced namespaces and referencing members of those namespaces through the :: qualifier, it is possible to guard against future code
The :: qualifier requires the left-hand identifier to be the identifier global (as explained below) or an extern or using alias that references a namespace. A compile-time error occurs if the alias references a type.
When the left-hand identifier of the :: qualifier is the identifier global, the global namespace (and only the global namespace) is searched for the right-hand identifier. For example:
class Program
{
static void
global::System.IO.Stream s = new global::MyLibrary.IO.EmptyStream();
...
}
}
Similar to the use of :: with extern and using aliases, the use of :: with the global identifier guarantees that the name lookup is unaffected by the introduction of new types and members. Note that the identifier global has special meaning only when used as the left-hand identifier of the :: qualifier. It is not a keyword and it is not itself an alias.
25.3.1 Qualified alias member
A qualified-alias-member is defined as follows:
qualified-alias-member:
identifier :: identifier type-argument-listopt
A new :: token is added to the C# lexical grammar. The updated form of the operator-or-punctuator lexical grammar production is shown in §20.10.
A qualified-alias-member can be used as a namespace-or-type-name (§20.9.1) or as the left operand in a member-access (§20.9.6).
A qualified-alias-member has one of two forms:
· N::I<A1, ..., AK>, where N and I represent identifiers, and <A1, ..., AK> is a type argument list. (K is always at least one.)
· N::I, where N and I represent identifiers. (In this case, K is considered to be zero.)
Using this notation, the meaning of a qualified-alias-member is determined as follows:
· If N is the identifier global, then the global namespace is searched for I:
o If the global namespace contains a namespace named N and K is zero, then the qualified-alias-member refers to that namespace.
o Otherwise, if the global namespace contains a non-generic type named I and K is zero, then the qualified-alias-member refers to that type.
o Otherwise, if the global namespace contains a type named I that has K type parameters, then the qualified-alias-member refers to that type constructed with the given type arguments.
o Otherwise, the qualified-alias-member is undefined and a compile-time error occurs.
· Otherwise, starting with the namespace declaration (§9.2) immediately containing the qualified-alias-member (if any), continuing with each enclosing namespace declaration (if any), and ending with the compilation unit containing the qualified-alias-member, the following steps are evaluated until an entity is located:
o If the namespace declaration or compilation unit contains a using-alias-directive that associates N with a type, then the qualified-alias-member is undefined and a compile-time error occurs.
o Otherwise, if the namespace declaration or compilation unit contains an extern-alias-directive or using-alias-directive that associates N with a namespace, then:
· If the namespace associated with N contains a namespace named I and K is zero, then the qualified-alias-member refers to that namespace.
· Otherwise, if the namespace associated with N contains a non-generic type named I and K is zero, then the qualified-alias-member refers to that type.
· Otherwise, if the namespace associated with N contains a type named I that has K type parameters, then the qualified-alias-member refers to that type constructed with the given type arguments.
· Otherwise, the qualified-alias-member is undefined and a compile-time error occurs.
· Otherwise, the qualified-alias-member is undefined and a compile-time error occurs.
Note that using the namespace alias qualifier with an alias that references a type causes a compile-time error. Also note that if the identifier N is global, then lookup is performed in the global namespace, even if there is a using alias associating global with a type or namespace.
25.3.2 Uniqueness of aliases
In C# 2.0, each compilation unit and namespace body has a separate declaration space for extern aliases and using aliases. Thus, while the name of an extern alias or using alias must be unique within the set of extern aliases and using aliases declared in the immediately containing compilation unit or namespace body, an alias is permitted to have the same name as a type or namespace as long as it is used only with the :: qualifier.
In the example
namespace N
{
public class A {}
public class B {}
}
namespace N
{
using A = System.IO;
class X
{
A.Stream s1; // Error, A is ambiguous
A::Stream s2; // Ok
}
}
the name A has two possible meanings in the second namespace body because both the class A and the using alias A are in scope. For this reason, use of A in the qualified name A.Stream is ambiguous and causes a compile-time error to occur. However, use of A with the :: qualifier is not an error because A is looked up only as a namespace alias.
25.4 Extern aliases
Until now, C# has supported only a single namespace hierarchy into which types from referenced assemblies and the current program are placed. Because of this design, it has not been possible to reference types with the same fully qualified name from different assemblies, a situation that arises when types are independently given the same name, or when a program needs to reference several versions of the same assembly. Extern aliases make it possible to create and reference separate namespace hierarchies in such situations.
Consider the following two assemblies:
Assembly a1.dll:
namespace N
{
public class A {}
public class B {}
}
Assembly a2.dll:
namespace N
{
public class B {}
public class C {}
}
and the following program:
class Test
{
N.A a; // Ok
N.B b; // Error
N.C c; // Ok
}
When the program is compiled with the command-line
csc /r:a1.dll /r:a2.dll test.cs
the types contained in a1.dll and a2.dll are all placed in the global namespace hierarchy, and an error occurs because the type N.B exists in both assemblies. With extern aliases, it becomes possible to place the types contained in a1.dll and a2.dll into separate namespace hierarchies.
The following program declares and uses two extern aliases, X and Y, each of which represent the root of a distinct namespace hierarchy created from the types contained in one or more assemblies.
extern alias X;
extern alias Y;
class Test
{
X::N.A a;
X::N.B b1;
Y::N.B b2;
Y::N.C c;
}
The program declares the existence of the extern aliases X and Y, but the actual definitions of the aliases are external to the program. With the command line compiler, the definition takes place when assemblies are referenced using the /r option. In
csc /r:X=a1.dll /r:Y=a2.dll test.cs
defines the extern alias X to be the root of a namespace hierarchy formed by the types in a1.dll and Y to be the root of a namespace hierarchy formed by the types in a2.dll. The identically named N.B classes can now be referenced as X.N.B and Y.N.B, or, using the namespace alias qualifier, X::N.B and Y::N.B. An error occurs if a program declares an extern alias for which no external definition is provided.
An extern alias can include multiple assemblies, and a particular assembly can be included in multiple extern aliases. For example, given the assembly
Assembly a3.dll:
namespace N
{
public class D {}
public class E {}
}
the command line
csc /r:X=a1.dll /r:X=a3.dll /r:Y=a2.dll /r:Y=a3.dll test.cs
defines the extern alias X to be the root of a namespace hierarchy formed by the types in a1.dll and a3.dll and Y to be the root of a namespace hierarchy formed by the types in a2.dll and a3.dll. Because of this definition, it is possible to refer to the class N.D in a3.dll as both X::N.D and Y::N.D.
An assembly can be placed in the global namespace hierarchy even if it is also included in one or more extern aliases. For example, the command line
csc /r:a1.dll /r:X=a1.dll /r:Y=a2.dll test.cs
places the assembly a1.dll in both the global namespace hierarchy and the namespace hierarchy rooted by the extern alias X. Consequently, the class N.A can be referred to as N.A or X::N.A.
It is possible to ensure that a lookup always starts at the root of the global namespace hierarchy by using the identifier global with the namespace alias qualifier, such as global::System.IO.Stream.
A using directive may reference an extern alias that was defined in the same immediately enclosing namespace declaration or compilation unit. For example:
extern alias X;
using X::N;
class Test
{
A a; // X::N.A
B b; // X::N.B
}
25.4.1 Extern alias directives
An extern-alias-directive introduces an identifier that serves as an alias for a namespace hierarchy.
compilation-unit:
extern-alias-directivesopt using-directivesopt global-attributesopt
namespace-member-declarationsopt
namespace-body:
{ extern-alias-directivesopt using-directivesopt namespace-member-declarationsopt }
extern-alias-directives:
extern-alias-directive
extern-alias-directives extern-alias-directive
extern-alias-directive:
extern alias identifier ;
The identifier of an extern-alias-directive must be unique within the set of extern aliases and using aliases declared in the immediately containing compilation unit or namespace body. This is described
The scope of an extern-alias-directive extends over the using-directives, global-attributes and namespace-member-declarations of its immediately containing compilation unit or namespace body. Within a compilation unit or namespace body that contains an extern-alias-directive, the identifier introduced by the extern-alias-directive can be used to reference the aliased namespace hierarchy.
Similar to a using-alias-directive (§9.3.1), an extern-alias-directive makes an alias available within a particular compilation unit or namespace body, but it does not contribute any new members to the underlying declaration space. In other words, an extern-alias-directive is not transitive, but, rather, affects only the compilation unit or namespace body in which it occurs.
Resolution of the namespace-or-type-name referenced by a using-alias-directive is not affected by the using-alias-directive itself or by other using-directives in the immediately containing compilation unit or namespace body, but may be affected by extern-alias-directives in the immediately containing compilation unit or namespace body. In other words, the namespace-or-type-name of a using-alias-directive is resolved as if the immediately containing compilation unit or namespace body had no using-directives but has the correct set of extern-alias-directives. In the example
namespace N1.N2 {}
namespace N3
{
extern alias E;
using R1 = E.N; // OK
using R2 = N1; // OK
using R3 = N1.N2; // OK
using R4 = R2.N2; // Error, R2 unknown
}
the last using-alias-directive results in a compile-time error because it is not affected by the first using-alias-directive. The first using-alias-directive does not result in an error since the scope of the extern alias E includes the using-alias-directive.
25.5 Pragma directives
The #pragma preprocessing directive is used to specify optional contextual information to the compiler. The information supplied in a #pragma directive will never change program semantics.
pp-directive:
…
pp-pragma
pp-pragma:
whitespaceopt # whitespaceopt pragma whitespace pragma-body pp-new-line
pragma-body:
pragma-warning-body
C# 2.0 provides #pragma directives to control compiler warnings. Future versions of the language may include additional #pragma directives. To ensure interoperability with other C# compilers, the Microsoft C# compiler does not issue compilation errors for unknown #pragma directives; such directives do however generate warnings.
25.5.1 Pragma warning
The #pragma warning directive is used to disable or restore all or a particular set of warning messages during compilation of the subsequent program
pragma-warning-body:
warning whitespace warning-action
warning whitespace warning-action whitespace warning-list
warning-action:
disable
restore
warning-list:
decimal-digits
warning-list whitespaceopt , whitespaceopt decimal-digits
A #pragma warning directive that omits the warning list affects all warnings. A #pragma warning directive the includes a warning list affects only those warnings that are specified in the list.
A #pragma warning disable directive disables all or the given set of warnings.
A #pragma warning restore directive restores all or the given set of warnings to the state that was in effect at the beginning of the compilation unit. Note that if a particular warning was disabled externally (for example using the command line compiler’s /nowarn option), a #pragma warning restore (whether for all or the specific warning) will not re-enable that warning.
The following example shows use of #pragma warning to temporarily disable the warning reported when obsoleted members are referenced.
using System;
class Program
{
[Obsolete]
static void Foo() {}
static void
#pragma warning disable 612
Foo();
#pragma warning restore 612
}
}
25.6 Default value expression
A default value expression is used to obtain the default value (§5.2) of a type. Typically a default value expression is used for type parameters, since it may not be known if the type parameter is a value type or a reference type. (No conversion exists from the null literal to a type parameter unless the type parameter is known to be a reference type.)
primary-no-array-creation-expression:
…
default-value-expression
default-value-expression:
default ( type )
If the type in a default-value-expression evaluates at run-time to a reference type, the result is null converted to that type. If the type in a default-value-expression evaluates at run-time to a value type, the result is the value-type’s default value (§4.1.2).
A default-value-expression is a constant expression (§7.15) if the type is a reference type or a type parameter that is known to be a reference type (§20.7). In addition, a default-value-expression is a constant expression if the type is one of the following value types: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, or any enumeration type.
25.7 Conditional attribute classes
An attribute class (§17.1) decorated with the Conditional attribute is said to be a conditional attribute class. The Conditional attribute indicates a condition by testing a conditional compilation symbol. Attribute specifications (§17.2) of a conditional attribute class are either included or omitted depending on whether this symbol is defined at the point of specification. If the symbol is defined, the attribute specification is included; otherwise the attribute specification is omitted.
The example
#define DEBUG
using System;
using System.Diagnostics;
[Conditional("DEBUG")]
public class TestAttribute : Attribute {}
[Test]
class C {}
declares the attribute class TestAttribute as a conditional attribute class, and subsequently applies a Test attribute to Class1. Since the conditional compilation symbol DEBUG is defined, retrieving the attributes applied to Class1 at runtime will result in an instance of the TestAttribute class. If the symbol DEBUG had not been defined, then retrieving the attributes applied to Class1 at runtime would not result in an instance of the TestAttribute class.
It is important to note that the inclusion or exclusion of an attribute specification of a conditional attribute class is controlled by the conditional compilation symbols at the point of the specification. In the example
File test.cs:
using System;
using System.Diagnostics;
[Conditional(“DEBUG”)]
public class TestAttribute : Attribute {}
File class1.cs:
#define DEBUG
[Test] // TestAttribute is included
class Class1 {}
File class2.cs:
#undef DEBUG
[Test] // TestAttribute is excluded
class Class2 {}
the classes Class1 and Class2 are each decorated with attribute Test, which is conditional based on whether or not DEBUG is defined. Since this symbol is defined in the context of Class1 but not Class2, the specification of the Test attribute on Class1 is included, while the specification of the Test attribute on Class2 is omitted.
25.8 Fixed size buffers
Fixed size buffers are used to declare “C style” in-line arrays as members of structs. Fixed size buffers are primarily useful for interfacing with unmanaged APIs. Fixed size buffers are an unsafe feature, and fixed size buffers can only be declared in unsafe contexts (§18.1).
25.8.1 Fixed size buffer declarations
A fixed size buffer is a member that represents storage for a fixed length buffer of variables of a given type. A fixed size buffer declaration introduces one or more fixed size buffers of a given element type. Fixed size buffers are only permitted in struct declarations and can only occur in unsafe contexts (§18.1).
struct-member-declaration:
…
fixed-size-buffer-declaration
fixed-size-buffer-declaration:
attributesopt fixed-size-buffer-modifiersopt fixed buffer-element-type
fixed-sized-buffer-declarators ;
fixed-size-buffer-modifiers:
fixed-size-buffer-modifier
fixed-sized-buffer-modifier fixed-size-buffer-modifiers
fixed-size-buffer-modifiers:
new
public
protected
internal
private
unsafe
buffer-element-type:
type
fixed-sized-buffer-declarators:
fixed-sized-buffer-declarator
fixed-sized-buffer-declarator fixed-sized-buffer-declarators
fixed-sized-buffer-declarator:
identifier [ const-expression ]
A fixed size buffer declaration may include a set of attributes (§17), a new modifier (§10.2.2), a valid combination of the four access modifiers (§10.2.3) and an unsafe modifier (§18.1). The attributes and modifiers apply to all of the members declared by the fixed size buffer declaration. It is an error for the same modifier to appear multiple times in a fixed sized buffer declaration.
A fixed size buffer declaration is not permitted to include the static modifier.
The buffer element type of a fixed sized buffer declaration specifies the element type of the buffer(s) introduced by the declaration. The buffer element type must be one of the predefined types sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, or bool.
The buffer element type is followed by a list of fixed size buffer declarators, each of which introduces a new member. A fixed size buffer declarator consists of an identifier that names the member, followed by a constant expression enclosed in [ and ] tokens. The constant expression denotes the number of elements in the member introduced by that fixed size buffer declarator. The type of the constant expression must be implicitly convertible to type int, and the value must be a non-zero positive integer.
The elements of a fixed size buffer are guaranteed to be laid out sequentially in memory.
A fixed size buffer declaration that declares multiple fixed size buffers is equivalent to multiple declarations of a single fixed sized buffer declation with the same attributes, and element types. For example
unsafe struct A
{
public fixed int x[5], y[10], z[100];
}
is equivalent to
unsafe struct A
{
public fixed int x[5];
public fixed int y[10];
public fixed int z[100];
}
25.8.2 Fixed size buffers in expressions
Member lookup (§7.3) of a fixed size buffer member proceeds exactly like member lookup of a field.
A fixed size buffer can be referenced in an expression using a simple-name (§7.5.2) or a member-access (§7.5.4).
When a fixed size buffer member is referenced as a simple name, the effect is the same as a member access of the form this.I, where I is the fixed size buffer member.
In a member access of the form E.I, if E is of a struct type and a member lookup of I in that struct type identifies a fixed size member, then E.I is evaluated an classified as follows:
· If the expression E.I does not occur in an unsafe context, a compile-time error occurs.
· If E is classified as a value, a compile-time error occurs.
· Otherwise, if E is a moveable variable (§18.3) and the expression E.I is not a fixed-pointer-initializer (§18.6), a compile-time error occurs.
· Otherwise, E references a fixed variable and the result of the expression is a pointer to the first element of the fixed size buffer member I in E. The result is of type S*, where S is the element type of I, and is classified as a value.
The subsequent elements of the fixed sized buffer can be accessed using pointer operations from the first element. Unlike access to arrays, access to the elements of a fixed size buffer is an unsafe operation and is not range checked.
The following example declares and uses a struct with a fixed size buffer member.
unsafe struct Font
{
public int size;
public fixed char name[32];
}
class Test
{
unsafe static void PutString(string s, char* buffer, int bufSize) {
int len = s.Length;
if (len > bufSize) len = bufSize;
for (int i = 0; i < len; i++) buffer[i] = s[i];
for (int i = len; i < bufSize; i++) buffer[i] = (char)0;
}
unsafe static void
{
Font f;
f.size = 10;
PutString("Times New Roman", f.name, 32);
}
}
25.8.3 Fixed statements
The fixed statement (§18.6) is extended to permit a fixed-pointer-initializer to be a simple-name or member-access that references a fixed size buffer member of a moveable variable. A fixed pointer initializer of this form produces a pointer to the first element of the fixed size buffer (§25.8.2), and the fixed size buffer is guaranteed to remain at a fixed address for the duration of the fixed statement.
For example:
unsafe struct Font
{
public int size;
public fixed char name[32];
}
class Test
{
unsafe static void PutString(string s, char* buffer, int bufSize) {
int len = s.Length;
if (len > bufSize) len = bufSize;
for (int i = 0; i < len; i++) buffer[i] = s[i];
for (int i = len; i < bufSize; i++) buffer[i] = (char)0;
}
Font f;
unsafe static void
{
Test test = new Test();
test.f.size = 10;
fixed (char* p = test.f.name) {
PutString("Times New Roman", p, 32);
}
}
}
25.8.4 Definite assignment checking
Fixed size buffers are not subject to definite assignment checking (§5.3), and fixed size buffer members are ignored for purposes of definite assignment checking of struct type variables.
When the outermost containing struct variable of a fixed size buffer member is a static variable, an instance variable of a class instance, or an array element, the elements of the fixed size buffer are automatically initialized to their default values (§5.2). In all other cases, the initial content of a fixed size buffer is undefined.