ЗНАЕТЕ ЛИ ВЫ?

Evaluation of anonymous function conversions to delegate types



Conversion of an anonymous function to a delegate type produces a delegate instance which references the anonymous function and the (possibly empty) set of captured outer variables that are active at the time of the evaluation. When the delegate is invoked, the body of the anonymous function 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 delegate produced from an anonymous function contains a single entry. The exact target object and target method of the delegate are unspecified. In particular, it is unspecified whether the target object of the delegate is null, the this value of the enclosing function member, or some other object.

Conversions of semantically identical anonymous functions with the same (possibly empty) set of captured outer variable instances to the same delegate types are permitted (but not required) to return the same delegate instance. The term semantically identical is used here to mean that execution of the anonymous functions will, in all cases, produce the same effects given the same arguments. This rule permits code such as the following to be optimized.

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, (double x) => Math.Sin(x));
b = Apply(b, (double y) => Math.Sin(y));
...
}
}

Since the two anonymous function delegates have the same (empty) set of captured outer variables, and since the anonymous functions are semantically identical, the compiler is permitted to have the delegates refer to the same target method. Indeed, the compiler is permitted to return the very same delegate instance from both anonymous function expressions.

Evaluation of anonymous function conversions to expression tree types

Conversion of an anonymous function to an expression tree type produces an expression tree (§4.6). More precisely, evaluation of the anonymous function conversion leads to the construction of an object structure that represents the structure of the anonymous function itself. The precise structure of the expression tree, as well as the exact process for creating it, are implementation defined.

Implementation example

This section describes a possible implementation of anonymous function conversions in terms of other 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. It only briefly mentions conversions to expression trees, as their exact semantics are outside the scope of this specification.

The remainder of this section gives several examples of code that contains anonymous functions with different characteristics. For each example, a corresponding translation to code that uses only other C# constructs is provided. In the examples, the identifier D is assumed by represent the following delegate type:

public delegate void D();

The simplest form of an anonymous function is one that captures no outer variables:

class Test
{
static void F() {
D d = () => { Console.WriteLine("test"); };
}
}

This can be translated to a delegate instantiation that references a compiler generated static method in which the code of the anonymous function is placed:

class Test
{
static void F() {
D d = new D(__Method1);
}

static void __Method1() {
Console.WriteLine("test");
}
}

In the following example, the anonymous function references instance members of this:

class Test
{
int x;

void F() {
D d = () => { Console.WriteLine(x); };
}
}

This can be translated to a compiler generated instance method containing the code of the anonymous function:

class Test
{
int x;

void F() {
D d = new D(__Method1);
}

void __Method1() {
Console.WriteLine(x);
}
}

In this example, the anonymous function captures a local variable:

class Test
{
void F() {
int y = 123;
D d = () => { Console.WriteLine(y); };
}
}

The lifetime of the local variable must now be extended to at least the lifetime of the anonymous function delegate. This can be achieved by “hoisting” the local variable into a field of a compiler generated class. Instantiation of the local variable (§7.15.5.2) then corresponds to creating an instance of the compiler generated class, and accessing the local variable corresponds to accessing a field in the instance of the compiler generated class. Furthermore, the anonymous function becomes an instance method of the compiler generated class:

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 function 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 = () => { Console.WriteLine(x + y + z); };
}
}
}

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 function 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);
}
}
}

The same technique applied here to capture local variables can also be used when converting anonymous functions to expression trees: References to the compiler generated objects can be stored in the expression tree, and access to the local variables can be represented as field accesses on these objects. The advantage of this approach is that it allows the “lifted” local variables to be shared between delegates and expression trees.

Method group conversions

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.5.3.1) to an argument list constructed by use of the parameter types and modifiers of D, as described in the following.

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 (§7.6.5.1) of the form E(A), with the following modifications:

o The argument list A is a list of expressions, each classified as a variable and with the type and modifier (ref or out) of the corresponding parameter in the formal-parameter-list of D.

o The candidate methods considered are only those methods that are applicable in their normal form (§7.5.3.1), not those applicable only in their expanded form.

· If the algorithm of §7.6.5.1 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 and the conversion is considered to exist.

· The selected method M must be compatible (§15.2) 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.

· If the selected method M is an extension method which is denoted by means of a member access on an instance expression, that instance expression 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.

Note that this process can lead to the creation of a delegate to an extension method, if the algorithm of §7.6.5.1 fails to find an instance method but succeeds in processing the invocation of E(A) as an extension method invocation (§7.6.5.2). A delegate thus created captures the extension method as well as its first argument.

The following example demonstrates method group conversions:

delegate string D1(object o);

delegate object D2(string s);

delegate object D3();

delegate string D4(object o, params object[] a);

delegate string D5(int i);

class Test
{
static string F(object o) {...}

static void G() {
D1 d1 = F; // Ok
D2 d2 = F; // Ok
D3 d3 = F; // Error – not applicable
D4 d4 = F; // Error – not applicable in normal form
D5 d5 = F; // Error – applicable but not compatible

}
}

The assignment to d1 implicitly converts the method group F to a value of type D1.

The assignment to d2 shows how it is possible to create a delegate to a method that has less derived (contra-variant) parameter types and a more derived (covariant) return type.

The assignment to d3 shows how no conversion exists if the method is not applicable.

The assignment to d4 shows how the method must be applicable in its normal form.

The assignment to d5 shows how parameter and return types of the delegate and method are allowed to differ only for reference types.

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 may influence overload resolution, and participate in type inference. See §7.5 for further details.

The run-time evaluation of a method group conversion proceeds as follows:

· If the method selected at compile-time is an instance method, or it is an extension method which is accessed as an instance method, the target object of the delegate is determined from the instance expression associated with E:

o The instance expression is evaluated. If this evaluation causes an exception, no further steps are executed.

o If the instance expression is of a reference-type, the value computed by the instance expression becomes the target object. If the selected method is an instance method and the target object is null, a System.NullReferenceException is thrown and no further steps are executed.

o If the instance expression is of a value-type, a boxing operation (§4.3.1) is performed to convert the value to an object, and this object becomes the target object.

· Otherwise the selected method is part of a static method call, and the target object of the delegate is null.

· A new instance of the delegate type D is allocated. If there is not enough memory available to allocate the new instance, a System.OutOfMemoryException is thrown and no further steps are executed.

· The new delegate instance is initialized with a reference to the method that was determined at compile-time and a reference to the target object computed above.

 


Expressions

An expression is a sequence of operators and operands. This chapter defines the syntax, order of evaluation of operands and operators, and meaning of expressions.

Expression classifications

An expression is classified as one of the following:

· A value. Every value has an associated type.

· A variable. Every variable has an associated type, namely the declared type of the variable.

· A namespace. An expression with this classification can only appear as the left hand side of a member-access (§7.6.4). In any other context, an expression classified as a namespace causes a compile-time error.

· A type. An expression with this classification can only appear as the left hand side of a member-access (§7.6.4), or as an operand for the as operator (§7.10.11), the is operator (§7.10.10), or the typeof operator (§7.6.11). In any other context, an expression classified as a type causes a compile-time error.

· A method group, which is a set of overloaded methods resulting from a member lookup (§7.4). A method group may have an associated instance expression and an associated type argument list. When an instance method is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.6.7). A method group is permitted in an invocation-expression (§7.6.5) , a delegate-creation-expression (§7.6.10.5) and as the left hand side of an is operator, and can be implicitly converted to a compatible delegate type (§6.6). In any other context, an expression classified as a method group causes a compile-time error.

· A null literal. An expression with this classification can be implicitly converted to a reference type or nullable type.

· An anonymous function. An expression with this classification can be implicitly converted to a compatible delegate type or expression tree type.

· A property access. Every property access has an associated type, namely the type of the property. Furthermore, a property access may have an associated instance expression. When an accessor (the get or set block) of an instance property access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.6.7).

· An event access. Every event access has an associated type, namely the type of the event. Furthermore, an event access may have an associated instance expression. An event access may appear as the left hand operand of the += and -= operators (§7.17.3). In any other context, an expression classified as an event access causes a compile-time error.

· An indexer access. Every indexer access has an associated type, namely the element type of the indexer. Furthermore, an indexer access has an associated instance expression and an associated argument list. When an accessor (the get or set block) of an indexer access is invoked, the result of evaluating the instance expression becomes the instance represented by this (§7.6.7), and the result of evaluating the argument list becomes the parameter list of the invocation.

· Nothing. This occurs when the expression is an invocation of a method with a return type of void. An expression classified as nothing is only valid in the context of a statement-expression (§8.6).

The final result of an expression is never a namespace, type, method group, or event access. Rather, as noted above, these categories of expressions are intermediate constructs that are only permitted in certain contexts.

A property access or indexer access is always reclassified as a value by performing an invocation of the get-accessor or the set-accessor. The particular accessor is determined by the context of the property or indexer access: If the access is the target of an assignment, the set-accessor is invoked to assign a new value (§7.17.1). Otherwise, the get-accessor is invoked to obtain the current value (§7.1.1).

Values of expressions

Most of the constructs that involve an expression ultimately require the expression to denote a value. In such cases, if the actual expression denotes a namespace, a type, a method group, or nothing, a compile-time error occurs. However, if the expression denotes a property access, an indexer access, or a variable, the value of the property, indexer, or variable is implicitly substituted:

· The value of a variable is simply the value currently stored in the storage location identified by the variable. A variable must be considered definitely assigned (§5.3) before its value can be obtained, or otherwise a compile-time error occurs.

· The value of a property access expression is obtained by invoking the get-accessor of the property. If the property has no get-accessor, a compile-time error occurs. Otherwise, a function member invocation (§7.5.4) is performed, and the result of the invocation becomes the value of the property access expression.

· The value of an indexer access expression is obtained by invoking the get-accessor of the indexer. If the indexer has no get-accessor, a compile-time error occurs. Otherwise, a function member invocation (§7.5.4) is performed with the argument list associated with the indexer access expression, and the result of the invocation becomes the value of the indexer access expression.

Static and Dynamic Binding

The process of determining the meaning of an operation based on the type or value of constituent expressions (arguments, operands, receivers) is often referred to as binding. For instance the meaning of a method call is determined based on the type of the receiver and arguments. The meaning of an operator is determined based on the type of its operands.

In C# the meaning of an operation is usually determined at compile-time, based on the compile-time type of its constituent expressions. Likewise, if an expression contains an error, the error is detected and reported by the compiler. This approach is known as static binding.

However, if an expression is a dynamic expression (i.e. has the type dynamic) this indicates that any binding that it participates in should be based on its run-time type (i.e. the actual type of the object it denotes at run-time) rather than the type it has at compile-time. The binding of such an operation is therefore deferred until the time where the operation is to be executed during the running of the program. This is referred to as dynamic binding.

When an operation is dynamically bound, little or no checking is performed by the compiler. Instead if the run-time binding fails, errors are reported as exceptions at run-time.

The following operations in C# are subject to binding:

· Member access: e.M

· Method invocation: e.M(e1,…,en)

· Delegate invocaton: e(e1,…,en)

· Element access: e[e1,…,en]

· Object creation: new C(e1,…,en)

· Overloaded unary operators: +, -, !, ~, ++, --, true, false

· Overloaded binary operators: +, -, *, /, %, &, &&, |, ||, ??, ^, <<, >>, ==,!=, >, <, >=, <=

· Assignment operators: =, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

· Implicit and explicit conversions

When no dynamic expressions are involved, C# defaults to static binding, which means that the compile-time types of constituent expressions are used in the selection process. However, when one of the constituent expressions in the operations listed above is a dynamic expression, the operation is instead dynamically bound.

Binding-time

Static binding takes place at compile-time, whereas dynamic binding takes place at run-time. In the following sections, the term binding-time refers to either compile-time or run-time, depending on when the binding takes place.

The following example illustrates the notions of static and dynamic binding and of binding-time:

object o = 5;
dynamic d = 5;

Console.WriteLine(5); // static binding to Console.WriteLine(int)
Console.WriteLine(o); // static binding to Console.WriteLine(object)
Console.WriteLine(d); // dynamic binding to Console.WriteLine(int)

The first two calls are statically bound: the overload of Console.WriteLine is picked based on the compile-time type of their argument. Thus, the binding-time is compile-time.

The third call is dynamically bound: the overload of Console.WriteLine is picked based on the run-time type of its argument. This happens because the argument is a dynamic expression – its compile-time type is dynamic. Thus, the binding-time for the third call is run-time.

Dynamic binding

The purpose of dynamic binding is to allow C# programs to interact with dynamic objects, i.e. objects that do not follow the normal rules of the C# type system. Dynamic objects may be objects from other programming languages with different types systems, or they may be objects that are programmatically setup to implement their own binding semantics for different operations.

The mechanism by which a dynamic object implements its own semantics is implementation defined. A given interface – again implementation defined – is implemented by dynamic objects to signal to the C# run-time that they have special semantics. Thus, whenever operations on a dynamic object are dynamically bound, their own binding semantics, rather than those of C# as specified in this document, take over.

While the purpose of dynamic binding is to allow interoperation with dynamic objects, C# allows dynamic binding on all objects, whether they are dynamic or not. This allows for a smoother integration of dynamic objects, as the results of operations on them may not themselves be dynamic objects, but are still of a type unknown to the programmer at compile-time. Also dynamic binding can help eliminate error-prone reflection-based code even when no objects involved are dynamic objects.

The following sections describe for each construct in the language exactly when dynamic binding is applied, what compile time checking – if any – is applied, and what the compile-time result and expression classification is.

7.2.3 Types of constituent expressions

When an operation is statically bound, the type of a constituent expression (e.g. a receiver, and argument, an index or an operand) is always considered to be the compile-time type of that expression.

When an operation is dynamically bound, the type of a constituent expression is determined in different ways depending on the compile-time type of the constituent expression:

· A constituent expression of compile-time type dynamic is considered to have the type of the actual value that the expression evaluates to at runtime

· A constituent expression whose compile-time type is a type parameter is considered to have the type which the type parameter is bound to at runtime

· Otherwise the constituent expression is considered to have its compile-time type.

Operators

Expressions are constructed from operands and operators. The operators of an expression indicate which operations to apply to the operands. Examples of operators include +, -, *, /, and new. Examples of operands include literals, fields, local variables, and expressions.

There are three kinds of operators:

· Unary operators. The unary operators take one operand and use either prefix notation (such as –x) or postfix notation (such as x++).

· Binary operators. The binary operators take two operands and all use infix notation (such as x + y).

· Ternary operator. Only one ternary operator, ?:, exists; it takes three operands and uses infix notation (c? x: y).

The order of evaluation of operators in an expression is determined by the precedence and associativity of the operators (§7.3.1).

Operands in an expression are evaluated from left to right. For example, in F(i) + G(i++) * H(i), method F is called using the old value of i, then method G is called with the old value of i, and, finally, method H is called with the new value of i. This is separate from and unrelated to operator precedence.

Certain operators can be overloaded. Operator overloading permits user-defined operator implementations to be specified for operations where one or both of the operands are of a user-defined class or struct type (§7.3.2).





Последнее изменение этой страницы: 2016-12-14; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 35.153.39.7 (0.021 с.)