Unsafe code
07/01/2017 39 minutes to read
The core C# language, as defined in the preceding chapters, differs notably from C and C++ in its omission of pointers as a data type. Instead, C# provides references and the ability to create objects that are managed by a garbage collector. This design, coupled with other features, makes C# a much safer language than C or C++. In the core C# language it is simply not possible to have an uninitialized variable, a "dangling" pointer, or an expression that indexes an array beyond its bounds. Whole categories of bugs that routinely plague C and C++ programs are thus eliminated.
While practically every pointer type construct in C or C++ has a reference type counterpart in C#, nonetheless, there are situations where access to pointer types becomes a necessity. For example, interfacing with the underlying operating system, accessing a memory-mapped device, or implementing a time-critical algorithm may not be possible or practical without access to pointers. To address this need, C# provides the ability to write unsafe code.
In unsafe code it is possible to declare and operate on pointers, to perform conversions between pointers and integral types, to take the address of variables, and so forth. In a sense, writing unsafe code is much like writing C code within a C# program.
Unsafe code is in fact a "safe" feature from the perspective of both developers and users. Unsafe code must be clearly marked with the modifier unsafe
, so developers can't possibly use unsafe features accidentally, and the execution engine works to ensure that unsafe code cannot be executed in an untrusted environment.
Unsafe contexts
The unsafe features of C# are available only in unsafe contexts. An unsafe context is introduced by including an unsafe
modifier in the declaration of a type or member, or by employing an unsafe_statement:
- A declaration of a class, struct, interface, or delegate may include an
unsafe
modifier, in which case the entire textual extent of that type declaration (including the body of the class, struct, or interface) is considered an unsafe context. - A declaration of a field, method, property, event, indexer, operator, instance constructor, destructor, or static constructor may include an
unsafe
modifier, in which case the entire textual extent of that member declaration is considered an unsafe context. - An unsafe_statement enables the use of an unsafe context within a block. The entire textual extent of the associated block is considered an unsafe context.
The associated grammar productions are shown below.
antlrCopy
class_modifier_unsafe
:
'unsafe'
;
struct_modifier_unsafe
:
'unsafe'
;
interface_modifier_unsafe
:
'unsafe'
;
delegate_modifier_unsafe
:
'unsafe'
;
field_modifier_unsafe
: 'unsafe'
;
method_modifier_unsafe : 'unsafe'
;
property_modifier_unsafe
: 'unsafe' ; event_modifier_unsafe : 'unsafe' ; indexer_modifier_unsafe : 'unsafe'; operator_modifier_unsafe : 'unsafe' ;
constructor_modifier_unsafe
:
'unsafe';
destructor_declaration_unsafe
: attributes?
'extern'?
'unsafe'?
'~'identifier
'('')'
destructor_body
| attributes?
'unsafe'?
'extern'?
'~'identifier
'('')'
destructor_body
;
static_constructor_modifiers_unsafe
:
'extern'?
'unsafe'?
'static'|
'unsafe'?
'extern'?
'static'|
'extern'?
'static''unsafe'
?
|
'unsafe'?
'static' 'extern'?
|
'static' 'extern'?
'unsafe'?
|
'static' 'unsafe'?
'extern'?
;
embedded_statement_unsafe
: unsafe_statement
| fixed_statement
unsafe_statement
:
'unsafe'block
;
In the example
C#Copy
public
unsafe
struct Node
{
public
int Value;
public
Node* Left;
public
Node* Right;
}
the unsafe
modifier specified in the struct declaration causes the entire textual extent of the struct declaration to become an unsafe context. Thus, it is possible to declare the Left
and Right
fields to be of a pointer type. The example above could also be written
C#Copy
public
struct Node
{
public
int Value;
public
unsafe Node* Left;
public
unsafe Node* Right;
}
Here, the unsafe
modifiers in the field declarations cause those declarations to be considered unsafe contexts.
Other than establishing an unsafe context, thus permitting the use of pointer types, the unsafe
modifier has no effect on a type or a member. In the example
C#Cop
public
class
A
{
public unsafe virtual void F()
{
char
* p;
...
}
}
public
class
B:
A
{
public override void F()
{
base
.F();
...
}
}
the unsafe
modifier on the F
method in A
simply causes the textual extent of F
to become an unsafe context in which the unsafe features of the language can be used. In the override of F
in B
, there is no need to re-specify the unsafe
modifier -- unless, of course, the F
method in B
itself needs access to unsafe features.
The situation is slightly different when a pointer type is part of the method's signature
C#Cop
public
unsafe
class
A
{
public virtual void F(char* p)
{...}
}
public
class
B:
A
{
public unsafe override void F(char* p)
{...}
}
Here, because F
's signature includes a pointer type, it can only be written in an unsafe context. However, the unsafe context can be introduced by either making the entire class unsafe, as is the case in A
, or by including an unsafe
modifier in the method declaration, as is the case in B
.
Pointer types
In an unsafe context, a type (Types) may be a pointer_type as well as a value_type or a reference_type. However, a pointer_type may also be used in a typeof
expression (Anonymous object creation expressions) outside of an unsafe context as such usage is not unsafe.
antlrCopy
type_unsafe
: pointer_type
;
A pointer_type is written as an unmanaged_type or the keyword void
, followed by a *
token:
antlrCopy
pointer_type
: unmanaged_type
'*'
|
'void'
'*'
;
unmanaged_type
: type
;
The type specified before the *
in a pointer type is called the referent type of the pointer type. It represents the type of the variable to which a value of the pointer type points.
Unlike references (values of reference types), pointers are not tracked by the garbage collector -- the garbage collector has no knowledge of pointers and the data to which they point. For this reason a pointer is not permitted to point to a reference or to a struct that contains references, and the referent type of a pointer must be an unmanaged_type.
An unmanaged_type is any type that isn't a reference_type or constructed type, and doesn't contain reference_type or constructed type fields at any level of nesting. In other words, an unmanaged_type is one of the following:
sbyte
,byte
,short
,ushort
,int
,uint
,long
,ulong
,char
,float
,double
,decimal
, orbool
.
- Any enum_type.
- Any pointer_type.
- Any user-defined struct_type that is not a constructed type and contains fields of unmanaged_types only.
The intuitive rule for mixing of pointers and references is that referents of references (objects) are permitted to contain pointers, but referents of pointers are not permitted to contain references.
Some examples of pointer types are given in the table below:
TABLE 1 | |
Example | Description |
| Pointer to |
| Pointer to |
| Pointer to pointer to |
| Single-dimensional array of pointers to |
| Pointer to unknown type |
For a given implementation, all pointer types must have the same size and representation.
Unlike C and C++, when multiple pointers are declared in the same declaration, in C# the *
is written along with the underlying type only, not as a prefix punctuator on each pointer name. For example
C#Copy
int* pi, pj;
// NOT as int *pi, *pj;
The value of a pointer having type T*
represents the address of a variable of type T
. The pointer indirection operator *
(Pointer indirection) may be used to access this variable. For example, given a variable P
of type int*
, the expression *P
denotes the int
variable found at the address contained in P
.
Like an object reference, a pointer may be null
. Applying the indirection operator to a null
pointer results in implementation-defined behavior. A pointer with value null
is represented by all-bits-zero.
The void*
type represents a pointer to an unknown type. Because the referent type is unknown, the indirection operator cannot be applied to a pointer of type void*
, nor can any arithmetic be performed on such a pointer. However, a pointer of type void*
can be cast to any other pointer type (and vice versa).
Pointer types are a separate category of types. Unlike reference types and value types, pointer types do not inherit from object
and no conversions exist between pointer types and object
. In particular, boxing and unboxing (Boxing and unboxing) are not supported for pointers. However, conversions are permitted between different pointer types and between pointer types and the integral types. This is described in Pointer conversions.
A pointer_type cannot be used as a type argument (Constructed types), and type inference (Type inference) fails on generic method calls that would have inferred a type argument to be a pointer type.
A pointer_type may be used as the type of a volatile field (Volatile fields).
Although pointers can be passed as ref
or out
parameters, doing so can cause undefined behavior, since the pointer may well be set to point to a local variable which no longer exists when the called method returns, or the fixed object to which it used to point, is no longer fixed. For example:
C#Copy
using System;
class
Test
{
static
int
value =
20;
unsafe static void F(out int* pi1, ref int* pi2)
{
int
i =
10;
pi1 = &i;
fixed
(
int* pj = &
value) {
// ...
pi2 = pj;
}
}
static void Main()
{
int
i =
10;
int
* px1;
int
* px2 = &i;
F(
out px1,
ref px2);
Console.WriteLine(
"*px1 = {0}, *px2 = {1}",
*px1, *px2);
// undefined behavior
}
}
}
A method can return a value of some type, and that type can be a pointer. For example, when given a pointer to a contiguous sequence of int
s, that sequence's element count, and some other int
value, the following method returns the address of that value in that sequence, if a match occurs; otherwise it returns null
:
C#Copy
unsafe
static
int* Find(
int* pi,
int size,
int
value)
{
for
(
int i =
0; i < size; ++i)
{
if
(*pi ==
value)
return
pi;
++pi;
}
return
null;
}
In an unsafe context, several constructs are available for operating on pointers:
- The
*
operator may be used to perform pointer indirection (Pointer indirection).
- The
->
operator may be used to access a member of a struct through a pointer (Pointer member access).
- The
[]
operator may be used to index a pointer (Pointer element access).
- The
&
operator may be used to obtain the address of a variable (The address-of operator).
- The
++
and--
operators may be used to increment and decrement pointers (Pointer increment and decrement).
- The
+
and-
operators may be used to perform pointer arithmetic (Pointer arithmetic).
- The
==
,!=
,<
,>
,<=
, and=>
operators may be used to compare pointers (Pointer comparison).
- The
stackalloc
operator may be used to allocate memory from the call stack (Fixed size buffers).
- The
fixed
statement may be used to temporarily fix a variable so its address can be obtained (The fixed statement).
Fixed and moveable variables
The address-of operator (The address-of operator) and the fixed
statement (The fixed statement) divide variables into two categories: Fixed variables and moveable variables.
Fixed variables reside in storage locations that are unaffected by operation of the garbage collector. (Examples of fixed variables include local variables, value parameters, and variables created by dereferencing pointers.) On the other hand, moveable variables reside in storage locations that are subject to relocation or disposal by the garbage collector. (Examples of moveable variables include fields in objects and elements of arrays.)
The &
operator (The address-of operator) permits the address of a fixed variable to be obtained without restrictions. However, because a moveable variable is subject to relocation or disposal by the garbage collector, the address of a moveable variable can only be obtained using a fixed
statement (The fixed statement), and that address remains valid only for the duration of that fixed
statement.
In precise terms, a fixed variable is one of the following:
- A variable resulting from a simple_name (Simple names) that refers to a local variable or a value parameter, unless the variable is captured by an anonymous function.
- A variable resulting from a member_access (Member access) of the form
V.I
, whereV
is a fixed variable of a struct_type.
- A variable resulting from a pointer_indirection_expression (Pointer indirection) of the form
*P
, a pointer_member_access (Pointer member access) of the formP->I
, or a pointer_element_access (Pointer element access) of the formP[E]
.
All other variables are classified as moveable variables.
Note that a static field is classified as a moveable variable. Also note that a ref
or out
parameter is classified as a moveable variable, even if the argument given for the parameter is a fixed variable. Finally, note that a variable produced by dereferencing a pointer is always classified as a fixed variable.
Pointer conversions
In an unsafe context, the set of available implicit conversions (Implicit conversions) is extended to include the following implicit pointer conversions:
- From any pointer_type to the type
void*
.
- From the
null
literal to any pointer_type.
Additionally, in an unsafe context, the set of available explicit conversions (Explicit conversions) is extended to include the following explicit pointer conversions:
- From any pointer_type to any other pointer_type.
- From
sbyte
,byte
,short
,ushort
,int
,uint
,long
, orulong
to any pointer_type.
- From any pointer_type to
sbyte
,byte
,short
,ushort
,int
,uint
,long
, orulong
.
Finally, in an unsafe context, the set of standard implicit conversions (Standard implicit conversions) includes the following pointer conversion:
- From any pointer_type to the type
void*
.
Conversions between two pointer types never change the actual pointer value. In other words, a conversion from one pointer type to another has no effect on the underlying address given by the pointer.
When one pointer type is converted to another, if the resulting pointer is not correctly aligned for the pointed-to type, the behavior is undefined if the result is dereferenced. In general, the concept "correctly aligned" is transitive: if a pointer to type A
is correctly aligned for a pointer to type B
, which, in turn, is correctly aligned for a pointer to type C
, then a pointer to type A
is correctly aligned for a pointer to type C
.
Consider the following case in which a variable having one type is accessed via a pointer to a different type:
C#Copy
char c =
'A';
char* pc = &c;
void* pv = pc;
int* pi = (int*)pv;
int i = *pi;
// undefined
*pi = 123456;
// undefined
When a pointer type is converted to a pointer to byte, the result points to the lowest addressed byte of the variable. Successive increments of the result, up to the size of the variable, yield pointers to the remaining bytes of that variable. For example, the following method displays each of the eight bytes in a double as a hexadecimal value:
C#Copy
using System;
class
Test
{
unsafe static void Main()
{
double
d =
123.456e23;
unsafe
{
byte
* pb = (
byte*)&d;
for
(
int i =
0; i <
sizeof(
double); ++i)
Console.Write(
"{0:X2} ", *pb++);
Console.WriteLine();
}
}
}
Of course, the output produced depends on endianness.
Mappings between pointers and integers are implementation-defined. However, on 32* and 64-bit CPU architectures with a linear address space, conversions of pointers to or from integral types typically behave exactly like conversions of uint
or ulong
values, respectively, to or from those integral types.
Pointer arrays
In an unsafe context, arrays of pointers can be constructed. Only some of the conversions that apply to other array types are allowed on pointer arrays:
- The implicit reference conversion (Implicit reference conversions) from any array_type to
System.Array
and the interfaces it implements also applies to pointer arrays. However, any attempt to access the array elements throughSystem.Array
or the interfaces it implements will result in an exception at run-time, as pointer types are not convertible toobject
.
- The implicit and explicit reference conversions (Implicit reference conversions, Explicit reference conversions) from a single-dimensional array type
S[]
toSystem.Collections.Generic.IList<T>
and its generic base interfaces never apply to pointer arrays, since pointer types cannot be used as type arguments, and there are no conversions from pointer types to non-pointer types.
- The explicit reference conversion (Explicit reference conversions) from
System.Array
and the interfaces it implements to any array_type applies to pointer arrays.
- The explicit reference conversions (Explicit reference conversions) from
System.Collections.Generic.IList<S>
and its base interfaces to a single-dimensional array typeT[]
never applies to pointer arrays, since pointer types cannot be used as type arguments, and there are no conversions from pointer types to non-pointer types.
These restrictions mean that the expansion for the foreach
statement over arrays described in The foreach statement cannot be applied to pointer arrays. Instead, a foreach statement of the form
C#Copy
foreach (V v
in x) embedded_statement
where the type of x
is an array type of the form T[,,...,]
, N
is the number of dimensions minus 1 and T
or V
is a pointer type, is expanded using nested for-loops as follows:
C#Copy
{
T[,,...,] a = x;
for (
int i0 = a.GetLowerBound(
0); i0 <= a.GetUpperBound(
0); i0++)
for
(
int i1 = a.GetLowerBound(
1); i1 <= a.GetUpperBound(
1); i1++)
...
for
(
int iN = a.GetLowerBound(N); iN <= a.GetUpperBound(N); iN++) {
V v = (V)a.GetValue(i0,i1,...,iN);
embedded_statement
}
}
The variables a
, i0
, i1
, ..., iN
are not visible to or accessible to x
or the embedded_statement or any other source code of the program. The variable v
is read-only in the embedded statement. If there is not an explicit conversion (Pointer conversions) from T
(the element type) to V
, an error is produced and no further steps are taken. If x
has the value null
, a System.NullReferenceException
is thrown at run-time.
Pointers in expressions
In an unsafe context, an expression may yield a result of a pointer type, but outside an unsafe context it is a compile-time error for an expression to be of a pointer type. In precise terms, outside an unsafe context a compile-time error occurs if any simple_name(Simple names), member_access (Member access), invocation_expression (Invocation expressions), or element_access (Element access) is of a pointer type.
In an unsafe context, the primary_no_array_creation_expression (Primary expressions) and unary_expression (Unary operators) productions permit the following additional constructs:
antlrCopy
primary_no_array_creation_expression_unsafe
: pointer_member_access
| pointer_element_access
| sizeof_expression
;
unary_expression_unsafe
: pointer_indirection_expression
| addressof_expression
;
These constructs are described in the following sections. The precedence and associativity of the unsafe operators is implied by the grammar.
Pointer indirection
A pointer_indirection_expression consists of an asterisk (*
) followed by a unary_expression.
antlrCopy
pointer_indirection_expression
:
'*'unary_expression;
The unary *
operator denotes pointer indirection and is used to obtain the variable to which a pointer points. The result of evaluating *P
, where P
is an expression of a pointer type T*
, is a variable of type T
. It is a compile-time error to apply the unary *
operator to an expression of type void*
or to an expression that isn't of a pointer type.
The effect of applying the unary *
operator to a null
pointer is implementation-defined. In particular, there is no guarantee that this operation throws a System.NullReferenceException
.
If an invalid value has been assigned to the pointer, the behavior of the unary *
operator is undefined. Among the invalid values for dereferencing a pointer by the unary *
operator are an address inappropriately aligned for the type pointed to (see example in Pointer conversions), and the address of a variable after the end of its lifetime.
For purposes of definite assignment analysis, a variable produced by evaluating an expression of the form *P
is considered initially assigned (Initially assigned variables).
Pointer member access
A pointer_member_access consists of a primary_expression, followed by a "->
" token, followed by an identifier and an optional type_argument_list.
antlrCopy
pointer_member_access
: primary_expression
'->' identifier;
In a pointer member access of the form P->I
, P
must be an expression of a pointer type other than void*
, and I
must denote an accessible member of the type to which P
points.
A pointer member access of the form P->I
is evaluated exactly as (*P).I
. For a description of the pointer indirection operator (*
), see Pointer indirection. For a description of the member access operator (.
), see Member access.
In the example
C#Copy
using System;
struct Point
{
public
int x;
public
int y;
public override string ToString()
{
return
"(" + x +
"," + y +
")";
}
}
class
Test
{
static void Main()
{
Point point;
unsafe
{
Point* p = &point;
p->x =
10;
p->y =
20;
Console.WriteLine(p->ToString());
}
}
}
the ->
operator is used to access fields and invoke a method of a struct through a pointer. Because the operation P->I
is precisely equivalent to (*P).I
, the Main
method could equally well have been written:
C#Copy
class
Test
{
static void Main()
{
Point point;
unsafe
{
Point* p = &point;
(*p).x =
10;
(*p).y =
20;
Console.WriteLine((*p).ToString());
}}
}
Pointer element access
A pointer_element_access consists of a primary_no_array_creation_expression followed by an expression enclosed in "[
" and "]
".
antlrCopy
pointer_element_access
: primary_no_array_creation_expression
'[' expression
']'
;
In a pointer element access of the form P[E]
, P
must be an expression of a pointer type other than void*
, and E
must be an expression that can be implicitly converted to int
, uint
, long
, or ulong
.
A pointer element access of the form P[E]
is evaluated exactly as *(P + E)
. For a description of the pointer indirection operator (*
), see Pointer indirection. For a description of the pointer addition operator (+
), see Pointer arithmetic.
In the example
C#Cop
class
Test{
static void Main()
{
unsafe
{
char
* p =
stackalloc
char[
256];
for
(
int i =
0; i <
256; i++) p[i] = (
char)i;
}
}
}
a pointer element access is used to initialize the character buffer in a for
loop. Because the operation P[E]
is precisely equivalent to *(P + E)
, the example could equally well have been written:
C#Copy
class
Test{
static void Main()
{
unsafe
{
char
* p =
stackalloc
char[
256];
for
(
int i =
0; i <
256; i++) *(p + i) = (
char)i;
}
}
}
The pointer element access operator does not check for out-of-bounds errors and the behavior when accessing an out-of-bounds element is undefined. This is the same as C and C++.
The address-of operator
An addressof_expression consists of an ampersand (&
) followed by a unary_expression.
antlrCopy
addressof_expression
:
'&' unary_expression
;
Given an expression E
which is of a type T
and is classified as a fixed variable (Fixed and moveable variables), the construct &E
computes the address of the variable given by E
. The type of the result is T*
and is classified as a value. A compile-time error occurs if E
is not classified as a variable, if E
is classified as a read-only local variable, or if E
denotes a moveable variable. In the last case, a fixed statement (The fixed statement) can be used to temporarily "fix" the variable before obtaining its address. As stated in Member access, outside an instance constructor or static constructor for a struct or class that defines a readonly
field, that field is considered a value, not a variable. As such, its address cannot be taken. Similarly, the address of a constant cannot be taken.
The &
operator does not require its argument to be definitely assigned, but following an &
operation, the variable to which the operator is applied is considered definitely assigned in the execution path in which the operation occurs. It is the responsibility of the programmer to ensure that correct initialization of the variable actually does take place in this situation.
In the example
C#Copy
using System;
class
Test
{
static void Main()
{
int
i;
unsafe
{
int
* p = &i;
*p =
123;
}
Console.WriteLine(i);
}
}
i
is considered definitely assigned following the &i
operation used to initialize p
. The assignment to *p
in effect initializes i
, but the inclusion of this initialization is the responsibility of the programmer, and no compile-time error would occur if the assignment was removed.
The rules of definite assignment for the &
operator exist such that redundant initialization of local variables can be avoided. For example, many external APIs take a pointer to a structure which is filled in by the API. Calls to such APIs typically pass the address of a local struct variable, and without the rule, redundant initialization of the struct variable would be required.
Pointer increment and decrement
In an unsafe context, the ++
and --
operators (Postfix increment and decrement operators and Prefix increment and decrement operators) can be applied to pointer variables of all types except void*
. Thus, for every pointer type T*
, the following operators are implicitly defined:
C#Copy
T*
operator ++(T* x);
T*
operator --(T* x);
The operators produce the same results as x + 1
and x - 1
, respectively (Pointer arithmetic). In other words, for a pointer variable of type T*
, the ++
operator adds sizeof(T)
to the address contained in the variable, and the --
operator subtracts sizeof(T)
from the address contained in the variable.
If a pointer increment or decrement operation overflows the domain of the pointer type, the result is implementation-defined, but no exceptions are produced.
Pointer arithmetic
In an unsafe context, the +
and -
operators (Addition operator and Subtraction operator) can be applied to values of all pointer types except void*
. Thus, for every pointer type T*
, the following operators are implicitly defined:
C#Copy
T*
operator +(T* x,
int y);
T*
operator +(T* x,
uint y);
T*
operator +(T* x,
long y);
T*
operator +(T* x,
ulong y);
T*
operator +(
int x, T* y);
T*
operator +(
uint x, T* y);
T*
operator +(
long x, T* y);
T*
operator +(
ulong x, T* y);
T*
operator -(T* x,
int y);
T*
operator -(T* x,
uint y);
T*
operator -(T* x,
long y);
T*
operator -(T* x,
ulong y);
long
operator -(T* x, T* y);
Given an expression P
of a pointer type T*
and an expression N
of type int
, uint
, long
, or ulong
, the expressions P + N
and N + P
compute the pointer value of type T*
that results from adding N * sizeof(T)
to the address given by P
. Likewise, the expression P - N
computes the pointer value of type T*
that results from subtracting N * sizeof(T)
from the address given by P
.
Given two expressions, P
and Q
, of a pointer type T*
, the expression P - Q
computes the difference between the addresses given by P
and Q
and then divides that difference by sizeof(T)
. The type of the result is always long
. In effect, P - Q
is computed as ((long)(P) - (long)(Q)) / sizeof(T)
.
For example:
C#Copy
using System;
class
Test
{
static void Main()
{
unsafe
{
int
* values =
stackalloc
int[
20];
int
* p = &values[
1];
int
* q = &values[
15];
Console.WriteLine(
"p - q = {0}", p - q);
Console.WriteLine(
"q - p = {0}", q - p);
}
}
}
which produces the output:
consoleCopy
p - q = -14
q - p = 14
If a pointer arithmetic operation overflows the domain of the pointer type, the result is truncated in an implementation-defined fashion, but no exceptions are produced.
Pointer comparison
In an unsafe context, the ==
, !=
, <
, >
, <=
, and =>
operators (Relational and type-testing operators) can be applied to values of all pointer types. The pointer comparison operators are:
C#Copy
bool
operator ==(
void* x,
void* y);
bool
operator !=(
void* x,
void* y);
bool
operator <(
void* x,
void* y);
bool
operator >(
void* x,
void* y);
bool
operator <=(
void* x,
void* y);
bool
operator >=(
void* x,
void* y);
Because an implicit conversion exists from any pointer type to the void*
type, operands of any pointer type can be compared using these operators. The comparison operators compare the addresses given by the two operands as if they were unsigned integers.
The sizeof operator
The sizeof
operator returns the number of bytes occupied by a variable of a given type. The type specified as an operand to sizeof
must be an unmanaged_type (Pointer types).
antlrCopy
sizeof_expression
:
'sizeof'
'(' unmanaged_type
')'
;
The result of the sizeof
operator is a value of type int
. For certain predefined types, the sizeof
operator yields a constant value as shown in the table below.
TABLE 2 | |
Expression | Result |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For all other types, the result of the sizeof
operator is implementation-defined and is classified as a value, not a constant.
The order in which members are packed into a struct is unspecified.
For alignment purposes, there may be unnamed padding at the beginning of a struct, within a struct, and at the end of the struct. The contents of the bits used as padding are indeterminate.
When applied to an operand that has struct type, the result is the total number of bytes in a variable of that type, including any padding.
The fixed statement
In an unsafe context, the embedded_statement (Statements) production permits an additional construct, the fixed
statement, which is used to "fix" a moveable variable such that its address remains constant for the duration of the statement.
antlrCopy
fixed_statement
:
'fixed'
'(' pointer_type fixed_pointer_declarators
')' embedded_statement
;
fixed_pointer_declarators
: fixed_pointer_declarator (
','fixed_pointer_declarator)*
;
fixed_pointer_declarator
: identifier
'='fixed_pointer_initializer
;
fixed_pointer_initializer
: '&'
variable_reference
| expression
;
Each fixed_pointer_declarator declares a local variable of the given pointer_type and initializes that local variable with the address computed by the corresponding fixed_pointer_initializer. A local variable declared in a fixed
statement is accessible in any fixed_pointer_initializers occurring to the right of that variable's declaration, and in the embedded_statement of the fixed
statement. A local variable declared by a fixed
statement is considered read-only. A compile-time error occurs if the embedded statement attempts to modify this local variable (via assignment or the ++
and --
operators) or pass it as a ref
or out
parameter.
A fixed_pointer_initializer can be one of the following:
- The token "
&
" followed by a variable_reference (Precise rules for determining definite assignment) to a moveable variable (Fixed and moveable variables) of an unmanaged typeT
, provided the typeT*
is implicitly convertible to the pointer type given in thefixed
statement. In this case, the initializer computes the address of the given variable, and the variable is guaranteed to remain at a fixed address for the duration of thefixed
statement.
- An expression of an array_type with elements of an unmanaged type
T
, provided the typeT*
is implicitly convertible to the pointer type given in thefixed
statement. In this case, the initializer computes the address of the first element in the array, and the entire array is guaranteed to remain at a fixed address for the duration of thefixed
statement. If the array expression is null or if the array has zero elements, the initializer computes an address equal to zero.
- An expression of type
string
, provided the typechar*
is implicitly convertible to the pointer type given in thefixed
statement. In this case, the initializer computes the address of the first character in the string, and the entire string is guaranteed to remain at a fixed address for the duration of thefixed
statement. The behavior of thefixed
statement is implementation-defined if the string expression is null.
- A simple_name or member_access that references a fixed size buffer member of a moveable variable, provided the type of the fixed size buffer member is implicitly convertible to the pointer type given in the
fixed
statement. In this case, the initializer computes a pointer to the first element of the fixed size buffer (Fixed size buffers in expressions), and the fixed size buffer is guaranteed to remain at a fixed address for the duration of thefixed
statement.
For each address computed by a fixed_pointer_initializer the fixed
statement ensures that the variable referenced by the address is not subject to relocation or disposal by the garbage collector for the duration of the fixed
statement. For example, if the address computed by a fixed_pointer_initializer references a field of an object or an element of an array instance, the fixed
statement guarantees that the containing object instance is not relocated or disposed of during the lifetime of the statement.
It is the programmer's responsibility to ensure that pointers created by fixed
statements do not survive beyond execution of those statements. For example, when pointers created by fixed
statements are passed to external APIs, it is the programmer's responsibility to ensure that the APIs retain no memory of these pointers.
Fixed objects may cause fragmentation of the heap (because they can't be moved). For that reason, objects should be fixed only when absolutely necessary and then only for the shortest amount of time possible.
The example
C#Copy
class
Test
{
static
int x;
int
y;
unsafe static void F(int* p)
{
*p =
1;
}
static void Main()
{
Test t =
new Test();
int
[] a =
new
int[
10];
unsafe
{
fixed
(
int* p = &x) F(p);
fixed
(
int* p = &t.y) F(p);
fixed
(
int* p = &a[
0]) F(p);
fixed
(
int* p = a) F(p);
}
}
}
demonstrates several uses of the fixed
statement. The first statement fixes and obtains the address of a static field, the second statement fixes and obtains the address of an instance field, and the third statement fixes and obtains the address of an array element. In each case it would have been an error to use the regular &
operator since the variables are all classified as moveable variables.
The fourth fixed
statement in the example above produces a similar result to the third.
This example of the fixed
statement uses string
:
C#Copy
class
Test
{
static
string name =
"xx";
unsafe static void F(char* p)
{
for
(
int i =
0; p[i] !=
'\0'; ++i)
Console.WriteLine(p[i]);
}
static void Main()
{
unsafe
{
fixed
(
char* p = name) F(p);
fixed
(
char* p =
"xx") F(p);
}
}
}
In an unsafe context array elements of single-dimensional arrays are stored in increasing index order, starting with index 0
and ending with index Length - 1
. For multi-dimensional arrays, array elements are stored such that the indices of the rightmost dimension are increased first, then the next left dimension, and so on to the left. Within a fixed
statement that obtains a pointer p
to an array instance a
, the pointer values ranging from p
to p + a.Length - 1
represent addresses of the elements in the array. Likewise, the variables ranging from p[0]
to p[a.Length - 1]
represent the actual array elements. Given the way in which arrays are stored, we can treat an array of any dimension as though it were linear.
For example:
C#Copy
using System;
class
Test
{
static void Main()
{
int
[,,] a =
new
int[
2,
3,
4];
unsafe
{
fixed
(
int* p = a) {
for
(
int i =
0; i < a.Length; ++i)
// treat as linear
p[i] = i;
}
}
for
(
int i =
0; i <
2; ++i)
for
(
int j =
0; j <
3; ++j) {
for
(
int k =
0; k <
4; ++k)
Console.Write(
"[{0},{1},{2}] = {3,2} ", i, j, k, a[i,j,k]);
Console.WriteLine();
}
}
}
which produces the output:
consoleCopy
[0,0,0] = 0 [0,0,1] = 1 [0,0,2] = 2 [0,0,3] = 3
[0,1,0] = 4 [0,1,1] = 5 [0,1,2] = 6 [0,1,3] = 7
[0,2,0] = 8 [0,2,1] = 9 [0,2,2] = 10 [0,2,3] = 11
[1,0,0] = 12 [1,0,1] = 13 [1,0,2] = 14 [1,0,3] = 15
[1,1,0] = 16 [1,1,1] = 17 [1,1,2] = 18 [1,1,3] = 19
[1,2,0] = 20 [1,2,1] = 21 [1,2,2] = 22 [1,2,3] = 23
In the example
C#Copy
class
Test
{
unsafe static void Fill(int* p, int count, int value)
{
for
(; count !=
0; count--) *p++ =
value;
}
static void Main()
{
int
[] a =
new
int[
100];
unsafe
{
fixed
(
int* p = a) Fill(p,
100,
-1);
}
}
}
a fixed
statement is used to fix an array so its address can be passed to a method that takes a pointer.
In the example:
C#Copy
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 Main()
{
Test test =
new Test();
test.f.size =
10;
fixed
(
char* p = test.f.name) {
PutString(
"Times New Roman", p,
32);
}
}
}
a fixed statement is used to fix a fixed size buffer of a struct so its address can be used as a pointer.
A char*
value produced by fixing a string instance always points to a null-terminated string. Within a fixed statement that obtains a pointer p
to a string instance s
, the pointer values ranging from p
to p + s.Length - 1
represent addresses of the characters in the string, and the pointer value p + s.Length
always points to a null character (the character with value '\0'
).
Modifying objects of managed type through fixed pointers can results in undefined behavior. For example, because strings are immutable, it is the programmer's responsibility to ensure that the characters referenced by a pointer to a fixed string are not modified.
The automatic null-termination of strings is particularly convenient when calling external APIs that expect "C-style" strings. Note, however, that a string instance is permitted to contain null characters. If such null characters are present, the string will appear truncated when treated as a null-terminated char*
.
Fixed size buffers
Fixed size buffers are used to declare "C style" in-line arrays as members of structs, and are primarily useful for interfacing with unmanaged APIs.
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 (Unsafe contexts).
antlrCopy
struct_member_declaration_unsafe
: fixed_size_buffer_declaration
;
fixed_size_buffer_declaration
: attributes? fixed_size_buffer_modifier*
'fixed'buffer_element_type fixed_size_buffer_declarator+
';' ;
fixed_size_buffer_modifier
:
'new'|
'public'|
'protected'|
'internal'|
'private'|
'unsafe';
buffer_element_type
: type ;
fixed_size_buffer_declarator
: identifier
'['constant_expression
']' ;
A fixed size buffer declaration may include a set of attributes (Attributes), a new
modifier (Modifiers), a valid combination of the four access modifiers (Type parameters and constraints) and an unsafe
modifier (Unsafe contexts). 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 size buffer declaration.
A fixed size buffer declaration is not permitted to include the static
modifier.
The buffer element type of a fixed size 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 size buffer declaration with the same attributes, and element types. For example
C#Copy
unsafe
struct A
{
public
fixed
int x[
5], y[
10], z[
100];
}
is equivalent to
C#Copy
unsafe
struct A
{
public
fixed
int x[
5];
public
fixed
int y[
10];
public
fixed
int z[
100];
}
Fixed size buffers in expressions
Member lookup (Operators) 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 (Type inference) or a member_access (Compile-time checking of dynamic overload resolution).
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 (Fixed and moveable variables) and the expressionE.I
is not a fixed_pointer_initializer(The fixed statement), 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 memberI
inE
. The result is of typeS*
, whereS
is the element type ofI
, and is classified as a value.
The subsequent elements of the fixed size 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.
C#Copy
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 Main()
{
Font f;
f.size =
10;
PutString(
"Times New Roman", f.name,
32);
}
}
Definite assignment checking
Fixed size buffers are not subject to definite assignment checking (Definite assignment), 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 (Default values). In all other cases, the initial content of a fixed size buffer is undefined.
Stack allocation
In an unsafe context, a local variable declaration (Local variable declarations) may include a stack allocation initializer which allocates memory from the call stack.
antlrCopy
local_variable_initializer_unsafe
: stackalloc_initializer
;
stackalloc_initializer
: 'stackalloc' unmanaged_type '['expression ']';
The unmanaged_type indicates the type of the items that will be stored in the newly allocated location, and the expression indicates the number of these items. Taken together, these specify the required allocation size. Since the size of a stack allocation cannot be negative, it is a compile-time error to specify the number of items as a constant_expression that evaluates to a negative value.
A stack allocation initializer of the form stackalloc T[E]
requires T
to be an unmanaged type (Pointer types) and E
to be an expression of type int
. The construct allocates E * sizeof(T)
bytes from the call stack and returns a pointer, of type T*
, to the newly allocated block. If E
is a negative value, then the behavior is undefined. If E
is zero, then no allocation is made, and the pointer returned is implementation-defined. If there is not enough memory available to allocate a block of the given size, a System.StackOverflowException
is thrown.
The content of the newly allocated memory is undefined.
Stack allocation initializers are not permitted in catch
or finally
blocks (The try statement).
There is no way to explicitly free memory allocated using stackalloc
. All stack allocated memory blocks created during the execution of a function member are automatically discarded when that function member returns. This corresponds to the alloca
function, an extension commonly found in C and C++ implementations.
In the example
C#Copy
using System;
class
Test
{
static string IntToString(int value)
{
int
n =
value >=
0?
value: -
value;
unsafe
{
char
* buffer =
stackalloc
char[
16];
char
* p = buffer +
16;
do
{
*--p = (
char)(n %
10 +
'0');
n /=
10;
}
while (n !=
0);
if
(
value <
0) *--p =
'-';
return
new
string(p,
0, (
int)(buffer +
16 - p));
}
}
static void Main() {
Console.WriteLine(IntToString(
12345));
Console.WriteLine(IntToString(
-999));
}
}
a stackalloc
initializer is used in the IntToString
method to allocate a buffer of 16 characters on the stack. The buffer is automatically discarded when the method returns.
Dynamic memory allocation
Except for the stackalloc
operator, C# provides no predefined constructs for managing non-garbage collected memory. Such services are typically provided by supporting class libraries or imported directly from the underlying operating system. For example, the Memory
class below illustrates how the heap functions of an underlying operating system might be accessed from C#:
C#Copy
using System;
using System.Runtime.InteropServices;
public
static
unsafe
class
Memory
{
// Handle for the process heap. This handle is used in all calls to the
// HeapXXX APIs in the methods below.
private
static
readonly IntPtr s_heap = GetProcessHeap();
// Allocates a memory block of the given size. The allocated memory is
// automatically initialized to zero.
public
static
void* Alloc(
int size)
{
void
* result = HeapAlloc(s_heap, HEAP_ZERO_MEMORY, (UIntPtr)size);
if
(result ==
null)
throw
new OutOfMemoryException();
return
result;
}
// Copies count bytes from src to dst. The source and destination
// blocks are permitted to overlap.
public static void Copy(void* src, void* dst, int count)
{
byte
* ps = (
byte*)src;
byte
* pd = (
byte*)dst;
if
(ps > pd)
{
for
(; count !=
0; count--) *pd++ = *ps++;
}
else
if (ps < pd)
{
for
(ps += count, pd += count; count !=
0; count--) *--pd = *--ps;
}
}
// Frees a memory block.
public static void Free(void* block)
{
if
(!HeapFree(s_heap,
0, block))
throw
new InvalidOperationException();
}
// Re-allocates a memory block. If the reallocation request is for a
// larger size, the additional region of memory is automatically
// initialized to zero.
public
static
void* ReAlloc(
void* block,
int size)
{
void
* result = HeapReAlloc(s_heap, HEAP_ZERO_MEMORY, block, (UIntPtr)size);
if
(result ==
null)
throw
new OutOfMemoryException();
return
result;
}
// Returns the size of a memory block.
public static int SizeOf(void* block)
{
int
result = (
int)HeapSize(s_heap,
0, block);
if
(result ==
-1)
throw
new InvalidOperationException();
return
result;
}
// Heap API flags
private
const
int HEAP_ZERO_MEMORY =
0x00000008;
// Heap API functions
[
DllImport("kernel32")]
private static extern IntPtr GetProcessHeap()
;
[
DllImport("kernel32")]
private
static
extern
void* HeapAlloc(IntPtr hHeap,
int flags, UIntPtr size);
[
DllImport("kernel32")]
private static extern bool HeapFree(IntPtr hHeap, int flags, void* block)
;
[
DllImport("kernel32")]
private
static
extern
void* HeapReAlloc(IntPtr hHeap,
int flags,
void* block, UIntPtr size);
[
DllImport("kernel32")]
private static extern UIntPtr HeapSize(IntPtr hHeap, int flags, void* block)
;
}
An example that uses the Memory
class is given below:
C#Copy
class
Test
{
static unsafe void Main()
{
byte
* buffer =
null;
try
{
const
int Size =
256;
buffer = (
byte*)Memory.Alloc(Size);
for
(
int i =
0; i < Size; i++) buffer[i] = (
byte)i;
byte
[] array =
new
byte[Size];
fixed
(
byte* p = array) Memory.Copy(buffer, p, Size);
for
(
int i =
0; i < Size; i++) Console.WriteLine(array[i]);
}
finally
{
if
(buffer !=
null) Memory.Free(buffer);
}
}
}
The example allocates 256 bytes of memory through Memory.Alloc
and initializes the memory block with values increasing from 0 to 255. It then allocates a 256 element byte array and uses Memory.Copy
to copy the contents of the memory block into the byte array. Finally, the memory block is freed using Memory.Free
and the contents of the byte array are output on the console.
///
Introduction
07/01/2017 55 minutes to read
C# (pronounced "See Sharp") is a simple, modern, object-oriented, and type-safe programming language. C# has its roots in the C family of languages and will be immediately familiar to C, C++, and Java programmers. C# is standardized by ECMA International as the ECMA-334 standard and by ISO/IEC as the ISO/IEC 23270 standard. Microsoft's C# compiler for the .NET Framework is a conforming implementation of both of these standards.
C# is an object-oriented language, but C# further includes support for component-oriented programming. Contemporary software design increasingly relies on software components in the form of self-contained and self-describing packages of functionality. Key to such components is that they present a programming model with properties, methods, and events; they have attributes that provide declarative information about the component; and they incorporate their own documentation. C# provides language constructs to directly support these concepts, making C# a very natural language in which to create and use software components.
Several C# features aid in the construction of robust and durable applications: Garbage collection automatically reclaims memory occupied by unused objects; exception handling provides a structured and extensible approach to error detection and recovery; and the type-safe design of the language makes it impossible to read from uninitialized variables, to index arrays beyond their bounds, or to perform unchecked type casts.
C# has a unified type system. All C# types, including primitive types such as int
and double
, inherit from a single root object
type. Thus, all types share a set of common operations, and values of any type can be stored, transported, and operated upon in a consistent manner. Furthermore, C# supports both user-defined reference types and value types, allowing dynamic allocation of objects as well as in-line storage of lightweight structures.
To ensure that C# programs and libraries can evolve over time in a compatible manner, much emphasis has been placed on versioning in C#'s design. Many programming languages pay little attention to this issue, and, as a result, programs written in those languages break more often than necessary when newer versions of dependent libraries are introduced. Aspects of C#'s design that were directly influenced by versioning considerations include the separate virtual
and override
modifiers, the rules for method overload resolution, and support for explicit interface member declarations.
The rest of this chapter describes the essential features of the C# language. Although later chapters describe rules and exceptions in a detail-oriented and sometimes mathematical manner, this chapter strives for clarity and brevity at the expense of completeness. The intent is to provide the reader with an introduction to the language that will facilitate the writing of early programs and the reading of later chapters.
Hello world
The "Hello, World" program is traditionally used to introduce a programming language. Here it is in C#:
C#Copy
using System;
class
Hello
{
static void Main()
{
Console.WriteLine(
"Hello, World");
}
}
C# source files typically have the file extension .cs
. Assuming that the "Hello, World" program is stored in the file hello.cs
, the program can be compiled with the Microsoft C# compiler using the command line
consoleCopy
csc hello.cs
which produces an executable assembly named hello.exe
. The output produced by this application when it is run is
consoleCopy
Hello, World
The "Hello, World" program starts with a using
directive that references the System
namespace. Namespaces provide a hierarchical means of organizing C# programs and libraries. Namespaces contain types and other namespaces—for example, the System
namespace contains a number of types, such as the Console
class referenced in the program, and a number of other namespaces, such as IO
and Collections
. A using
directive that references a given namespace enables unqualified use of the types that are members of that namespace. Because of the using
directive, the program can use Console.WriteLine
as shorthand for System.Console.WriteLine
.
The Hello
class declared by the "Hello, World" program has a single member, the method named Main
. The Main
method is declared with the static
modifier. While instance methods can reference a particular enclosing object instance using the keyword this
, static methods operate without reference to a particular object. By convention, a static method named Main
serves as the entry point of a program.
The output of the program is produced by the WriteLine
method of the Console
class in the System
namespace. This class is provided by the .NET Framework class libraries, which, by default, are automatically referenced by the Microsoft C# compiler. Note that C# itself does not have a separate runtime library. Instead, the .NET Framework is the runtime library of C#.
Program structure
The key organizational concepts in C# are programs, namespaces, types, members, and assemblies. C# programs consist of one or more source files. Programs declare types, which contain members and can be organized into namespaces. Classes and interfaces are examples of types. Fields, methods, properties, and events are examples of members. When C# programs are compiled, they are physically packaged into assemblies. Assemblies typically have the file extension .exe
or .dll
, depending on whether they implement applications or libraries.
The example
C#Copy
using System;
namespace
Acme.Collections
{
public
class
Stack
{
Entry top;
public void Push(object data)
{
top =
new Entry(top, data);
}
public object Pop()
{
if
(top ==
null)
throw
new InvalidOperationException();
object result = top.data;
top = top.next;
return result;
}
class
Entry
{
public
Entry next;
public
object data;
public Entry(Entry next, object data)
{
this
.next = next;
this.data = data;
}
}
}
}
declares a class named Stack
in a namespace called Acme.Collections
. The fully qualified name of this class is Acme.Collections.Stack
. The class contains several members: a field named top
, two methods named Push
and Pop
, and a nested class named Entry
. The Entry
class further contains three members: a field named next
, a field named data
, and a constructor. Assuming that the source code of the example is stored in the file acme.cs
, the command line
consoleCopy
csc /t:library acme.cs
compiles the example as a library (code without a Main
entry point) and produces an assembly named acme.dll
.
Assemblies contain executable code in the form of Intermediate Language (IL) instructions, and symbolic information in the form of metadata. Before it is executed, the IL code in an assembly is automatically converted to processor-specific code by the Just-In-Time (JIT) compiler of .NET Common Language Runtime.
Because an assembly is a self-describing unit of functionality containing both code and metadata, there is no need for #include
directives and header files in C#. The public types and members contained in a particular assembly are made available in a C# program simply by referencing that assembly when compiling the program. For example, this program uses the Acme.Collections.Stack
class from the acme.dll
assembly:
C#Copy
using System;
using Acme.Collections;
class
Test{
static void Main()
{
Stack s =
new Stack();
s.Push(
1);
s.Push(
10);
s.Push(
100);
Console.WriteLine(s.Pop());
Console.WriteLine(s.Pop());
Console.WriteLine(s.Pop());
}
}
If the program is stored in the file test.cs
, when test.cs
is compiled, the acme.dll
assembly can be referenced using the compiler's /r
option:
consoleCopy
csc /r:acme.dll test.cs
This creates an executable assembly named test.exe
, which, when run, produces the output:
consoleCopy
100
10
1
C# permits the source text of a program to be stored in several source files. When a multi-file C# program is compiled, all of the source files are processed together, and the source files can freely reference each other—conceptually, it is as if all the source files were concatenated into one large file before being processed. Forward declarations are never needed in C# because, with very few exceptions, declaration order is insignificant. C# does not limit a source file to declaring only one public type nor does it require the name of the source file to match a type declared in the source file.
Types and variables
There are two kinds of types in C#: value types and reference types. Variables of value types directly contain their data whereas variables of reference types store references to their data, the latter being known as objects. With reference types, it is possible for two variables to reference the same object and thus possible for operations on one variable to affect the object referenced by the other variable. With value types, the variables each have their own copy of the data, and it is not possible for operations on one to affect the other (except in the case of ref
and out
parameter variables).
C#'s value types are further divided into simple types, enum types, struct types, and nullable types, and C#'s reference types are further divided into class types, interface types, array types, and delegate types.
The following table provides an overview of C#'s type system.
TABLE 1 | ||
Category | Description | |
Value types | Simple types | Signed integral: |
Unsigned integral: | ||
Unicode characters: | ||
IEEE floating point: | ||
High-precision decimal: | ||
Boolean: | ||
Enum types | User-defined types of the form | |
Struct types | User-defined types of the form | |
Nullable types | Extensions of all other value types with a | |
Reference types | Class types | Ultimate base class of all other types: |
Unicode strings: | ||
User-defined types of the form | ||
Interface types | User-defined types of the form | |
Array types | Single- and multi-dimensional, for example, | |
Delegate types | User-defined types of the form e.g. |
The eight integral types provide support for 8-bit, 16-bit, 32-bit, and 64-bit values in signed or unsigned form.
The two floating point types, float
and double
, are represented using the 32-bit single-precision and 64-bit double-precision IEEE 754 formats.
The decimal
type is a 128-bit data type suitable for financial and monetary calculations.
C#'s bool
type is used to represent boolean values—values that are either true
or false
.
Character and string processing in C# uses Unicode encoding. The char
type represents a UTF-16 code unit, and the string
type represents a sequence of UTF-16 code units.
The following table summarizes C#'s numeric types.
TABLE 2 | |||
Category | Bits | Type | Range/Precision |
Signed integral | 8 |
| -128...127 |
16 |
| -32,768...32,767 | |
32 |
| -2,147,483,648...2,147,483,647 | |
64 |
| -9,223,372,036,854,775,808...9,223,372,036,854,775,807 | |
Unsigned integral | 8 |
| 0...255 |
16 |
| 0...65,535 | |
32 |
| 0...4,294,967,295 | |
64 |
| 0...18,446,744,073,709,551,615 | |
Floating point | 32 |
| 1.5 × 10^−45 to 3.4 × 10^38, 7-digit precision |
64 |
| 5.0 × 10^−324 to 1.7 × 10^308, 15-digit precision | |
Decimal | 128 |
| 1.0 × 10^−28 to 7.9 × 10^28, 28-digit precision |
C# programs use type declarations to create new types. A type declaration specifies the name and the members of the new type. Five of C#'s categories of types are user-definable: class types, struct types, interface types, enum types, and delegate types.
A class type defines a data structure that contains data members (fields) and function members (methods, properties, and others). Class types support single inheritance and polymorphism, mechanisms whereby derived classes can extend and specialize base classes.
A struct type is similar to a class type in that it represents a structure with data members and function members. However, unlike classes, structs are value types and do not require heap allocation. Struct types do not support user-specified inheritance, and all struct types implicitly inherit from type object
.
An interface type defines a contract as a named set of public function members. A class or struct that implements an interface must provide implementations of the interface's function members. An interface may inherit from multiple base interfaces, and a class or struct may implement multiple interfaces.
A delegate type represents references to methods with a particular parameter list and return type. Delegates make it possible to treat methods as entities that can be assigned to variables and passed as parameters. Delegates are similar to the concept of function pointers found in some other languages, but unlike function pointers, delegates are object-oriented and type-safe.
Class, struct, interface and delegate types all support generics, whereby they can be parameterized with other types.
An enum type is a distinct type with named constants. Every enum type has an underlying type, which must be one of the eight integral types. The set of values of an enum type is the same as the set of values of the underlying type.
C# supports single- and multi-dimensional arrays of any type. Unlike the types listed above, array types do not have to be declared before they can be used. Instead, array types are constructed by following a type name with square brackets. For example, int[]
is a single-dimensional array of int
, int[,]
is a two-dimensional array of int
, and int[][]
is a single-dimensional array of single-dimensional arrays of int
.
Nullable types also do not have to be declared before they can be used. For each non-nullable value type T
there is a corresponding nullable type T?
, which can hold an additional value null
. For instance, int?
is a type that can hold any 32 bit integer or the value null
.
C#'s type system is unified such that a value of any type can be treated as an object. Every type in C# directly or indirectly derives from the object
class type, and object
is the ultimate base class of all types. Values of reference types are treated as objects simply by viewing the values as type object
. Values of value types are treated as objects by performing boxing and unboxing operations. In the following example, an int
value is converted to object
and back again to int
.
C#Copy
using System;
class
Test
{
static void Main() {
int
i =
123;
object
o = i;
// Boxing
int
j = (
int)o;
// Unboxing
}
}
When a value of a value type is converted to type object
, an object instance, also called a "box," is allocated to hold the value, and the value is copied into that box. Conversely, when an object
reference is cast to a value type, a check is made that the referenced object is a box of the correct value type, and, if the check succeeds, the value in the box is copied out.
C#'s unified type system effectively means that value types can become objects "on demand." Because of the unification, general-purpose libraries that use type object
can be used with both reference types and value types.
There are several kinds of variables in C#, including fields, array elements, local variables, and parameters. Variables represent storage locations, and every variable has a type that determines what values can be stored in the variable, as shown by the following table.
TABLE 3 | |
Type of Variable | Possible Contents |
Non-nullable value type | A value of that exact type |
Nullable value type | A null value or a value of that exact type |
| A null reference, a reference to an object of any reference type, or a reference to a boxed value of any value type |
Class type | A null reference, a reference to an instance of that class type, or a reference to an instance of a class derived from that class type |
Interface type | A null reference, a reference to an instance of a class type that implements that interface type, or a reference to a boxed value of a value type that implements that interface type |
Array type | A null reference, a reference to an instance of that array type, or a reference to an instance of a compatible array type |
Delegate type | A null reference or a reference to an instance of that delegate type |
Expressions
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.
When an expression contains multiple operators, the precedence of the operators controls the order in which the individual operators are evaluated. For example, the expression x + y * z
is evaluated as x + (y * z)
because the *
operator has higher precedence than the +
operator.
Most 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.
The following table summarizes C#'s operators, listing the operator categories in order of precedence from highest to lowest. Operators in the same category have equal precedence.
TABLE 4 | ||
Category | Expression | Description |
Primary |
| Member access |
| Method and delegate invocation | |
| Array and indexer access | |
| Post-increment | |
| Post-decrement | |
| Object and delegate creation | |
| Object creation with initializer | |
| Anonymous object initializer | |
| Array creation | |
| Obtain | |
| Evaluate expression in checked context | |
| Evaluate expression in unchecked context | |
| Obtain default value of type | |
| Anonymous function (anonymous method) | |
Unary |
| Identity |
| Negation | |
| Logical negation | |
| Bitwise negation | |
| Pre-increment | |
| Pre-decrement | |
| Explicitly convert | |
| Asynchronously wait for | |
Multiplicative |
| Multiplication |
| Division | |
| Remainder | |
Additive |
| Addition, string concatenation, delegate combination |
| Subtraction, delegate removal | |
Shift |
| Shift left |
| Shift right | |
Relational and type testing |
| Less than |
| Greater than | |
| Less than or equal | |
| Greater than or equal | |
| Return | |
| Return | |
Equality |
| Equal |
| Not equal | |
Logical AND |
| Integer bitwise AND, boolean logical AND |
Logical XOR |
| Integer bitwise XOR, boolean logical XOR |
Logical OR |
| Integer bitwise OR, boolean logical OR |
Conditional AND |
| Evaluates |
Conditional OR |
| Evaluates |
Null coalescing |
| Evaluates to |
Conditional |
| Evaluates |
Assignment or anonymous function |
| Assignment |
| Compound assignment; supported operators are | |
| Anonymous function (lambda expression) |
Statements
The actions of a program are expressed using statements. C# supports several different kinds of statements, a number of which are defined in terms of embedded statements.
A block permits multiple statements to be written in contexts where a single statement is allowed. A block consists of a list of statements written between the delimiters {
and }
.
Declaration statements are used to declare local variables and constants.
Expression statements are used to evaluate expressions. Expressions that can be used as statements include method invocations, object allocations using the new
operator, assignments using =
and the compound assignment operators, increment and decrement operations using the ++
and --
operators and await expressions.
Selection statements are used to select one of a number of possible statements for execution based on the value of some expression. In this group are the if
and switch
statements.
Iteration statements are used to repeatedly execute an embedded statement. In this group are the while
, do
, for
, and foreach
statements.
Jump statements are used to transfer control. In this group are the break
, continue
, goto
, throw
, return
, and yield
statements.
The try
...catch
statement is used to catch exceptions that occur during execution of a block, and the try
...finally
statement is used to specify finalization code that is always executed, whether an exception occurred or not.
The checked
and unchecked
statements are used to control the overflow checking context for integral-type arithmetic operations and conversions.
The lock
statement is used to obtain the mutual-exclusion lock for a given object, execute a statement, and then release the lock.
The using
statement is used to obtain a resource, execute a statement, and then dispose of that resource.
Below are examples of each kind of statement
Local variable declarations
C#Copy
static void Main() {
int
a;
int
b =
2, c =
3;
a =
1;
Console.WriteLine(a + b + c);
}
Local constant declaration
C#Copy
static void Main() {
const
float pi =
3.1415927f;
const
int r =
25;
Console.WriteLine(pi * r * r);
}
Expression statement
C#Copy
static void Main() {
int
i;
i =
123;
// Expression statement
Console.WriteLine(i);
// Expression statement
i++;
// Expression statement
Console.WriteLine(i);
// Expression statement
}
if
statement
C#Copy
static void Main(string[] args) {
if
(args.Length ==
0) {
Console.WriteLine(
"No arguments");
}
else
{
Console.WriteLine(
"One or more arguments");
}
}
switch
statement
C#Copy
static void Main(string[] args) {
int
n = args.Length;
switch
(n) {
case
0:
Console.WriteLine(
"No arguments");
break
;
case
1:
Console.WriteLine(
"One argument");
break
;
default
:
Console.WriteLine(
"{0} arguments", n);
break
;
}
}
while
statement
C#Copy
static void Main(string[] args) {
int
i =
0;
while
(i < args.Length) {
Console.WriteLine(args[i]);
i++;
}
}
do
statement
C#Copy
static void Main() {
string
s;
do
{
s = Console.ReadLine();
if
(s !=
null) Console.WriteLine(s);
}
while (s !=
null);
}
for
statement
C#Copy
static void Main(string[] args) {
for
(
int i =
0; i < args.Length; i++) {
Console.WriteLine(args[i]);
}
}
foreach
statement
C#Copy
static void Main(string[] args) {
foreach
(
string s
in args) {
Console.WriteLine(s);
}
}
break
statement
C#Copy
static void Main() {
while
(
true) {
string
s = Console.ReadLine();
if
(s ==
null)
break;
Console.WriteLine(s);
}
}
continue
statement
C#Copy
static void Main(string[] args) {
for (
int i =
0; i < args.Length; i++) {
if (args[i].StartsWith(
"/"))
continue;
Console.WriteLine(args[i]);
}
}
goto
statement
C#Copy
static void Main(string[] args) {
int i =
0;
goto check;
loop:
Console.WriteLine(args[i++]);
check:
if (i < args.Length)
goto loop;
}
return
statement
C#Copy
static int Add(int a, int b) {
return a + b;
}
static void Main() { Console.WriteLine(Add(1, 2)); return; }
yield
statement
C#Copy
static IEnumerable<int> Range(int from, int to) {
for (
int i =
from; i < to; i++) {
yield
return i;
}
yield
break;
}
static void Main() { foreach (int x in Range(-10,10)) { Console.WriteLine(x); } }
throw
and try
statements
C#Copy
static double Divide(double x, double y) {
if (y ==
0)
throw
new DivideByZeroException();
return x / y;
}
static void Main(string[] args) { try { if (args.Length != 2) { throw new Exception("Two numbers required"); } double x = double.Parse(args[0]); double y = double.Parse(args[1]); Console.WriteLine(Divide(x, y)); } catch (Exception e) { Console.WriteLine(e.Message); } finally { Console.WriteLine("Good bye!"); } }
checked
and unchecked
statements
C#Copy
static void Main() {
int i =
int.MaxValue;
checked {
Console.WriteLine(i +
1);
// Exception
}
unchecked {
Console.WriteLine(i +
1);
// Overflow
}
}
lock
statement
C#Copy
class
Account
{
decimal balance;
public void Withdraw(decimal amount) {
lock (
this) {
if (amount > balance) {
throw
new Exception(
"Insufficient funds");
}
balance -= amount;
}
}
}
using
statement
C#Copy
static void Main() {
using (TextWriter w = File.CreateText(
"test.txt")) {
w.WriteLine(
"Line one");
w.WriteLine(
"Line two");
w.WriteLine(
"Line three");
}
}
Classes and objects
Classes are the most fundamental of C#'s types. A class is a data structure that combines state (fields) and actions (methods and other function members) in a single unit. A class provides a definition for dynamically created instances of the class, also known as objects. Classes support inheritance and polymorphism, mechanisms whereby derived classes can extend and specialize base classes.
New classes are created using class declarations. A class declaration starts with a header that specifies the attributes and modifiers of the class, the name of the class, the base class (if given), and the interfaces implemented by the class. The header is followed by the class body, which consists of a list of member declarations written between the delimiters {
and }
.
The following is a declaration of a simple class named Point
:
C#Copy
public
class
Point{
public
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Instances of classes are created using the new
operator, which allocates memory for a new instance, invokes a constructor to initialize the instance, and returns a reference to the instance. The following statements create two Point
objects and store references to those objects in two variables:
C#Copy
Point p1 =
new Point(
0,
0);
Point p2 =
new Point(
10,
20);
The memory occupied by an object is automatically reclaimed when the object is no longer in use. It is neither necessary nor possible to explicitly deallocate objects in C#.
Members
The members of a class are either static members or instance members. Static members belong to classes, and instance members belong to objects (instances of classes).
The following table provides an overview of the kinds of members a class can contain.
TABLE 5 | |
Member | Description |
Constants | Constant values associated with the class |
Fields | Variables of the class |
Methods | Computations and actions that can be performed by the class |
Properties | Actions associated with reading and writing named properties of the class |
Indexers | Actions associated with indexing instances of the class like an array |
Events | Notifications that can be generated by the class |
Operators | Conversions and expression operators supported by the class |
Constructors | Actions required to initialize instances of the class or the class itself |
Destructors | Actions to perform before instances of the class are permanently discarded |
Types | Nested types declared by the class |
Accessibility
Each member of a class has an associated accessibility, which controls the regions of program text that are able to access the member. There are five possible forms of accessibility. These are summarized in the following table.
TABLE 6 | |
Accessibility | Meaning |
| Access not limited |
| Access limited to this class or classes derived from this class |
| Access limited to this program |
| Access limited to this program or classes derived from this class |
| Access limited to this class |
Type parameters
A class definition may specify a set of type parameters by following the class name with angle brackets enclosing a list of type parameter names. The type parameters can the be used in the body of the class declarations to define the members of the class. In the following example, the type parameters of Pair
are TFirst
and TSecond
:
C#Copy
public
class
Pair<
TFirst,
TSecond>
{
public TFirst First;
public TSecond Second;
}
A class type that is declared to take type parameters is called a generic class type. Struct, interface and delegate types can also be generic.
When the generic class is used, type arguments must be provided for each of the type parameters:
C#Copy
Pair<
int,
string> pair =
new Pair<
int,
string> { First =
1, Second =
"two" };
int i = pair.First;
// TFirst is int
string s = pair.Second;
// TSecond is string
A generic type with type arguments provided, like Pair<int,string>
above, is called a constructed type.
Base classes
A class declaration may specify a base class by following the class name and type parameters with a colon and the name of the base class. Omitting a base class specification is the same as deriving from type object
. In the following example, the base class of Point3D
is Point
, and the base class of Point
is object
:
C#Copy
public
class
Point{
public
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public
class
Point3D:
Point{
public
int z;
public Point3D(int x, int y, int z): base(x, y) {
this.z = z;
}
}
A class inherits the members of its base class. Inheritance means that a class implicitly contains all members of its base class, except for the instance and static constructors, and the destructors of the base class. A derived class can add new members to those it inherits, but it cannot remove the definition of an inherited member. In the previous example, Point3D
inherits the x
and y
fields from Point
, and every Point3D
instance contains three fields, x
, y
, and z
.
An implicit conversion exists from a class type to any of its base class types. Therefore, a variable of a class type can reference an instance of that class or an instance of any derived class. For example, given the previous class declarations, a variable of type Point
can reference either a Point
or a Point3D
:
C#Copy
Point a =
new Point(
10,
20);
Point b =
new Point3D(
10,
20,
30);
Fields
A field is a variable that is associated with a class or with an instance of a class.
A field declared with the static
modifier defines a static field. A static field identifies exactly one storage location. No matter how many instances of a class are created, there is only ever one copy of a static field.
A field declared without the static
modifier defines an instance field. Every instance of a class contains a separate copy of all the instance fields of that class.
In the following example, each instance of the Color
class has a separate copy of the r
, g
, and b
instance fields, but there is only one copy of the Black
, White
, Red
, Green
, and Blue
static fields:
C#Copy
public
class
Color{
public
static
readonly Color Black =
new Color(
0,
0,
0);
public
static
readonly Color White =
new Color(
255,
255,
255);
public
static
readonly Color Red =
new Color(
255,
0,
0);
public
static
readonly Color Green =
new Color(
0,
255,
0);
public
static
readonly Color Blue =
new Color(
0,
0,
255);
private
byte r, g, b;
public Color(byte r, byte g, byte b) {
this.r = r;
this.g = g;
this.b = b;
}
}
As shown in the previous example, read-only fields may be declared with a readonly
modifier. Assignment to a readonly
field can only occur as part of the field's declaration or in a constructor in the same class.
Methods
A method is a member that implements a computation or action that can be performed by an object or class. Static methods are accessed through the class. Instance methods are accessed through instances of the class.
Methods have a (possibly empty) list of parameters, which represent values or variable references passed to the method, and a return type, which specifies the type of the value computed and returned by the method. A method's return type is void
if it does not return a value.
Like types, methods may also have a set of type parameters, for which type arguments must be specified when the method is called. Unlike types, the type arguments can often be inferred from the arguments of a method call and need not be explicitly given.
The signature of a method must be unique in the class in which the method is declared. The signature of a method consists of the name of the method, the number of type parameters and the number, modifiers, and types of its parameters. The signature of a method does not include the return type.
Parameters
Parameters are used to pass values or variable references to methods. The parameters of a method get their actual values from the arguments that are specified when the method is invoked. There are four kinds of parameters: value parameters, reference parameters, output parameters, and parameter arrays.
A value parameter is used for input parameter passing. A value parameter corresponds to a local variable that gets its initial value from the argument that was passed for the parameter. Modifications to a value parameter do not affect the argument that was passed for the parameter.
Value parameters can be optional, by specifying a default value so that corresponding arguments can be omitted.
A reference parameter is used for both input and output parameter passing. The argument passed for a reference parameter must be a variable, and during execution of the method, the reference parameter represents the same storage location as the argument variable. A reference parameter is declared with the ref
modifier. The following example shows the use of ref
parameters.
C#Copy
using System;
class
Test
{
static void Swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
static void Main() {
int i =
1, j =
2;
Swap(
ref i,
ref j);
Console.WriteLine(
"{0} {1}", i, j);
// Outputs "2 1"
}
}
An output parameter is used for output parameter passing. An output parameter is similar to a reference parameter except that the initial value of the caller-provided argument is unimportant. An output parameter is declared with the out
modifier. The following example shows the use of out
parameters.
C#Copy
using System;
class
Test{
static void Divide(int x, int y, out int result, out int remainder) {
result = x / y;
remainder = x % y;
}
static void Main() {
int res, rem;
Divide(
10,
3,
out res,
out rem);
Console.WriteLine(
"{0} {1}", res, rem);
// Outputs "3 1"
}
}
A parameter array permits a variable number of arguments to be passed to a method. A parameter array is declared with the params
modifier. Only the last parameter of a method can be a parameter array, and the type of a parameter array must be a single-dimensional array type. The Write
and WriteLine
methods of the System.Console
class are good examples of parameter array usage. They are declared as follows.
C#Copy
public
class
Console{
public static void Write(string fmt, params object[] args) {...}
public static void WriteLine(string fmt, params object[] args) {...}
...
}
Within a method that uses a parameter array, the parameter array behaves exactly like a regular parameter of an array type. However, in an invocation of a method with a parameter array, it is possible to pass either a single argument of the parameter array type or any number of arguments of the element type of the parameter array. In the latter case, an array instance is automatically created and initialized with the given arguments. This example
C#Copy
Console.WriteLine(
"x={0} y={1} z={2}", x, y, z);
is equivalent to writing the following.
C#Copy
string s =
"x={0} y={1} z={2}";
object[] args =
new
object[
3];
args[
0] = x;
args[
1] = y;
args[
2] = z;
Console.WriteLine(s, args);
Method body and local variables
A method's body specifies the statements to execute when the method is invoked.
A method body can declare variables that are specific to the invocation of the method. Such variables are called local variables. A local variable declaration specifies a type name, a variable name, and possibly an initial value. The following example declares a local variable i
with an initial value of zero and a local variable j
with no initial value.
C#Copy
using System;
class
Squares{
static void Main() {
int i =
0;
int j;
while (i <
10) {
j = i * i;
Console.WriteLine(
"{0} x {0} = {1}", i, j);
i = i +
1;
}
}
}
C# requires a local variable to be definitely assigned before its value can be obtained. For example, if the declaration of the previous i
did not include an initial value, the compiler would report an error for the subsequent usages of i
because i
would not be definitely assigned at those points in the program.
A method can use return
statements to return control to its caller. In a method returning void
, return
statements cannot specify an expression. In a method returning non-void
, return
statements must include an expression that computes the return value.
Static and instance methods
A method declared with a static
modifier is a static method. A static method does not operate on a specific instance and can only directly access static members.
A method declared without a static
modifier is an instance method. An instance method operates on a specific instance and can access both static and instance members. The instance on which an instance method was invoked can be explicitly accessed as this
. It is an error to refer to this
in a static method.
The following Entity
class has both static and instance members.
C#Copy
class
Entity{
static
int nextSerialNo;
int serialNo;
public Entity() {
serialNo = nextSerialNo++;
}
public int GetSerialNo() {
return serialNo;
}
public static int GetNextSerialNo() {
return nextSerialNo;
}
public static void SetNextSerialNo(int value) {
nextSerialNo =
value;
}
}
Each Entity
instance contains a serial number (and presumably some other information that is not shown here). The Entity
constructor (which is like an instance method) initializes the new instance with the next available serial number. Because the constructor is an instance member, it is permitted to access both the serialNo
instance field and the nextSerialNo
static field.
The GetNextSerialNo
and SetNextSerialNo
static methods can access the nextSerialNo
static field, but it would be an error for them to directly access the serialNo
instance field.
The following example shows the use of the Entity
class.
C#Copy
using System;
class
Test{
static void Main() {
Entity.SetNextSerialNo(
1000);
Entity e1 =
new Entity();
Entity e2 =
new Entity();
Console.WriteLine(e1.GetSerialNo());
// Outputs "1000"
Console.WriteLine(e2.GetSerialNo());
// Outputs "1001"
Console.WriteLine(Entity.GetNextSerialNo());
// Outputs "1002"
}
}
Note that the SetNextSerialNo
and GetNextSerialNo
static methods are invoked on the class whereas the GetSerialNo
instance method is invoked on instances of the class.
Virtual, override, and abstract methods
When an instance method declaration includes a virtual
modifier, the method is said to be a virtual method. When no virtual
modifier is present, the method is said to be a non-virtual method.
When a virtual method is invoked, the run-time type of the instance for which that invocation takes place determines the actual method implementation to invoke. In a nonvirtual method invocation, the compile-time type of the instance is the determining factor.
A virtual method can be overridden in a derived class. When an instance method declaration includes an override
modifier, the method overrides an inherited virtual method with the same signature. Whereas a virtual method declaration introduces a new method, an override method declaration specializes an existing inherited virtual method by providing a new implementation of that method.
An abstract method is a virtual method with no implementation. An abstract method is declared with the abstract
modifier and is permitted only in a class that is also declared abstract
. An abstract method must be overridden in every non-abstract derived class.
The following example declares an abstract class, Expression
, which represents an expression tree node, and three derived classes, Constant
, VariableReference
, and Operation
, which implement expression tree nodes for constants, variable references, and arithmetic operations. (This is similar to, but not to be confused with the expression tree types introduced in Expression tree types).
C#Copy
using System;
using System.Collections;
public
abstract
class
Expression{
public abstract double Evaluate(Hashtable vars);
}
public
class
Constant:
Expression{
double
value;
public Constant(double value) {
this.
value =
value;
}
public override double Evaluate(Hashtable vars) {
return
value;
}
}
public
class
VariableReference:
Expression{
string name;
public VariableReference(string name) {
this.name = name;
}
public override double Evaluate(Hashtable vars) {
object
value = vars[name];
if (
value ==
null) {
throw
new Exception(
"Unknown variable: " + name);
}
return Convert.ToDouble(
value);
}
}
public
class
Operation:
Expression{
Expression left;
char op;
Expression right;
public Operation(Expression left, char op, Expression right) {
this.left = left;
this.op = op;
this.right = right;
}
public override double Evaluate(Hashtable vars) {
double x = left.Evaluate(vars);
double y = right.Evaluate(vars);
switch (op) {
case
'+':
return x + y;
case
'-':
return x - y;
case
'*':
return x * y;
case
'/':
return x / y;
}
throw
new Exception(
"Unknown operator");
}
}
The previous four classes can be used to model arithmetic expressions. For example, using instances of these classes, the expression x + 3
can be represented as follows.
C#Copy
Expression e =
new Operation(
new VariableReference(
"x"),
'+',
new Constant(
3));
The Evaluate
method of an Expression
instance is invoked to evaluate the given expression and produce a double
value. The method takes as an argument a Hashtable
that contains variable names (as keys of the entries) and values (as values of the entries). The Evaluate
method is a virtual abstract method, meaning that non-abstract derived classes must override it to provide an actual implementation.
A Constant
's implementation of Evaluate
simply returns the stored constant. A VariableReference
's implementation looks up the variable name in the hashtable and returns the resulting value. An Operation
's implementation first evaluates the left and right operands (by recursively invoking their Evaluate
methods) and then performs the given arithmetic operation.
The following program uses the Expression
classes to evaluate the expression x * (y + 2)
for different values of x
and y
.
C#Copy
using System;
using System.Collections;
class
Test{
static void Main() {
Expression e =
new Operation(
new VariableReference(
"x"),
'*',
new Operation(
new VariableReference(
"y"),
'+',
new Constant(
2)
)
);
Hashtable vars =
new Hashtable();
vars[
"x"] =
3;
vars[
"y"] =
5;
Console.WriteLine(e.Evaluate(vars));
// Outputs "21"
vars[
"x"] =
1.5;
vars[
"y"] =
9;
Console.WriteLine(e.Evaluate(vars));
// Outputs "16.5"
}
}
Method overloading
Method overloading permits multiple methods in the same class to have the same name as long as they have unique signatures. When compiling an invocation of an overloaded method, the compiler uses overload resolution to determine the specific method to invoke. Overload resolution finds the one method that best matches the arguments or reports an error if no single best match can be found. The following example shows overload resolution in effect. The comment for each invocation in the Main
method shows which method is actually invoked.
C#Copy
class
Test{
static void F() {
Console.WriteLine(
"F()");
}
static void F(object x) {
Console.WriteLine(
"F(object)");
}
static void F(int x) {
Console.WriteLine(
"F(int)");
}
static void F(double x) {
Console.WriteLine(
"F(double)");
}
static
void F<T>(T x) {
Console.WriteLine(
"F<T>(T)");
}
static void F(double x, double y) {
Console.WriteLine(
"F(double, double)");
}
static void Main() {
F();
// Invokes F()
F(
1);
// Invokes F(int)
F(
1.0);
// Invokes F(double)
F(
"abc");
// Invokes F(object)
F((
double)
1);
// Invokes F(double)
F((
object)
1);
// Invokes F(object)
F<
int>(
1);
// Invokes F<T>(T)
F(
1,
1);
// Invokes F(double, double)
}
}
As shown by the example, a particular method can always be selected by explicitly casting the arguments to the exact parameter types and/or explicitly supplying type arguments.
Other function members
Members that contain executable code are collectively known as the function members of a class. The preceding section describes methods, which are the primary kind of function members. This section describes the other kinds of function members supported by C#: constructors, properties, indexers, events, operators, and destructors.
The following code shows a generic class called List<T>
, which implements a growable list of objects. The class contains several examples of the most common kinds of function members.
C#Copy
public
class
List<
T> {
// Constant...
const
int defaultCapacity =
4;
// Fields...
T[] items;
int count;
// Constructors...
public List(int capacity = defaultCapacity) {
items =
new T[capacity];
}
// Properties...
public
int Count {
get {
return count; }
}
public
int Capacity {
get {
return items.Length;
}
set {
if (
value < count)
value = count;
if (
value != items.Length) {
T[] newItems =
new T[
value];
Array.Copy(items,
0, newItems,
0, count);
items = newItems;
}
}
}
// Indexer...
public T
this[
int index] {
get {
return items[index];
}
set {
items[index] =
value;
OnChanged();
}
}
// Methods...
public void Add(T item) {
if (count == Capacity) Capacity = count *
2;
items[count] = item;
count++;
OnChanged();
}
protected virtual void OnChanged() {
if (Changed !=
null) Changed(
this, EventArgs.Empty);
}
public override bool Equals(object other) {
return Equals(
this, other
as List<T>);
}
static bool Equals(List<T> a, List<T> b) {
if (a ==
null)
return b ==
null;
if (b ==
null || a.count != b.count)
return
false;
for (
int i =
0; i < a.count; i++) {
if (!
object.Equals(a.items[i], b.items[i])) {
return
false;
}
}
return
true;
}
// Event...
public
event EventHandler Changed;
// Operators...
public
static
bool
operator ==(List<T> a, List<T> b) {
return Equals(a, b);
}
public
static
bool
operator !=(List<T> a, List<T> b) {
return !Equals(a, b);
}
}
Constructors
C# supports both instance and static constructors. An instance constructor is a member that implements the actions required to initialize an instance of a class. A static constructor is a member that implements the actions required to initialize a class itself when it is first loaded.
A constructor is declared like a method with no return type and the same name as the containing class. If a constructor declaration includes a static
modifier, it declares a static constructor. Otherwise, it declares an instance constructor.
Instance constructors can be overloaded. For example, the List<T>
class declares two instance constructors, one with no parameters and one that takes an int
parameter. Instance constructors are invoked using the new
operator. The following statements allocate two List<string>
instances using each of the constructors of the List
class.
C#Copy
List<
string> list1 =
new List<
string>();
List<
string> list2 =
new List<
string>(
10);
Unlike other members, instance constructors are not inherited, and a class has no instance constructors other than those actually declared in the class. If no instance constructor is supplied for a class, then an empty one with no parameters is automatically provided.
Properties
Properties are a natural extension of fields. Both are named members with associated types, and the syntax for accessing fields and properties is the same. However, unlike fields, properties do not denote storage locations. Instead, properties have accessors that specify the statements to be executed when their values are read or written.
A property is declared like a field, except that the declaration ends with a get
accessor and/or a set
accessor written between the delimiters {
and }
instead of ending in a semicolon. A property that has both a get
accessor and a set
accessor is a read-write property, a property that has only a get
accessor is a read-only property, and a property that has only a set
accessor is a write-only property.
A get
accessor corresponds to a parameterless method with a return value of the property type. Except as the target of an assignment, when a property is referenced in an expression, the get
accessor of the property is invoked to compute the value of the property.
A set
accessor corresponds to a method with a single parameter named value
and no return type. When a property is referenced as the target of an assignment or as the operand of ++
or --
, the set
accessor is invoked with an argument that provides the new value.
The List<T>
class declares two properties, Count
and Capacity
, which are read-only and read-write, respectively. The following is an example of use of these properties.
C#Copy
List<
string> names =
new List<
string>();
names.Capacity =
100;
// Invokes set accessor
int i = names.Count;
// Invokes get accessor
int j = names.Capacity;
// Invokes get accessor
Similar to fields and methods, C# supports both instance properties and static properties. Static properties are declared with the static
modifier, and instance properties are declared without it.
The accessor(s) of a property can be virtual. When a property declaration includes a virtual
, abstract
, or override
modifier, it applies to the accessor(s) of the property.
Indexers
An indexer is a member that enables objects to be indexed in the same way as an array. An indexer is declared like a property except that the name of the member is this
followed by a parameter list written between the delimiters [
and ]
. The parameters are available in the accessor(s) of the indexer. Similar to properties, indexers can be read-write, read-only, and write-only, and the accessor(s) of an indexer can be virtual.
The List
class declares a single read-write indexer that takes an int
parameter. The indexer makes it possible to index List
instances with int
values. For example
C#Copy
List<
string> names =
new List<
string>();
names.Add(
"Liz");
names.Add(
"Martha");
names.Add(
"Beth");
for (
int i =
0; i < names.Count; i++) {
string s = names[i];
names[i] = s.ToUpper();
}
Indexers can be overloaded, meaning that a class can declare multiple indexers as long as the number or types of their parameters differ.
Events
An event is a member that enables a class or object to provide notifications. An event is declared like a field except that the declaration includes an event
keyword and the type must be a delegate type.
Within a class that declares an event member, the event behaves just like a field of a delegate type (provided the event is not abstract and does not declare accessors). The field stores a reference to a delegate that represents the event handlers that have been added to the event. If no event handles are present, the field is null
.
The List<T>
class declares a single event member called Changed
, which indicates that a new item has been added to the list. The Changed
event is raised by the OnChanged
virtual method, which first checks whether the event is null
(meaning that no handlers are present). The notion of raising an event is precisely equivalent to invoking the delegate represented by the event—thus, there are no special language constructs for raising events.
Clients react to events through event handlers. Event handlers are attached using the +=
operator and removed using the -=
operator. The following example attaches an event handler to the Changed
event of a List<string>
.
C#Copy
using System;
class
Test{
static
int changeCount;
static void ListChanged(object sender, EventArgs e) {
changeCount++;
}
static void Main() {
List<
string> names =
new List<
string>();
names.Changed +=
new EventHandler(ListChanged);
names.Add(
"Liz");
names.Add(
"Martha");
names.Add(
"Beth");
Console.WriteLine(changeCount);
// Outputs "3"
}
}
For advanced scenarios where control of the underlying storage of an event is desired, an event declaration can explicitly provide add
and remove
accessors, which are somewhat similar to the set
accessor of a property.
Operators
An operator is a member that defines the meaning of applying a particular expression operator to instances of a class. Three kinds of operators can be defined: unary operators, binary operators, and conversion operators. All operators must be declared as public
and static
.
The List<T>
class declares two operators, operator==
and operator!=
, and thus gives new meaning to expressions that apply those operators to List
instances. Specifically, the operators define equality of two List<T>
instances as comparing each of the contained objects using their Equals
methods. The following example uses the ==
operator to compare two List<int>
instances.
C#Copy
using System;
class
Test{
static void Main() {
List<
int> a =
new List<
int>();
a.Add(
1);
a.Add(
2);
List<
int> b =
new List<
int>();
b.Add(
1);
b.Add(
2);
Console.WriteLine(a == b);
// Outputs "True"
b.Add(
3);
Console.WriteLine(a == b);
// Outputs "False"
}
}
The first Console.WriteLine
outputs True
because the two lists contain the same number of objects with the same values in the same order. Had List<T>
not defined operator==
, the first Console.WriteLine
would have output False
because a
and b
reference different List<int>
instances.
Destructors
A destructor is a member that implements the actions required to destruct an instance of a class. Destructors cannot have parameters, they cannot have accessibility modifiers, and they cannot be invoked explicitly. The destructor for an instance is invoked automatically during garbage collection.
The garbage collector is allowed wide latitude in deciding when to collect objects and run destructors. Specifically, the timing of destructor invocations is not deterministic, and destructors may be executed on any thread. For these and other reasons, classes should implement destructors only when no other solutions are feasible.
The using
statement provides a better approach to object destruction.
Structs
Like classes, structs are data structures that can contain data members and function members, but unlike classes, structs are value types and do not require heap allocation. A variable of a struct type directly stores the data of the struct, whereas a variable of a class type stores a reference to a dynamically allocated object. Struct types do not support user-specified inheritance, and all struct types implicitly inherit from type object
.
Structs are particularly useful for small data structures that have value semantics. Complex numbers, points in a coordinate system, or key-value pairs in a dictionary are all good examples of structs. The use of structs rather than classes for small data structures can make a large difference in the number of memory allocations an application performs. For example, the following program creates and initializes an array of 100 points. With Point
implemented as a class, 101 separate objects are instantiated—one for the array and one each for the 100 elements.
C#Copy
class
Point{
public
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class
Test{
static void Main() {
Point[] points =
new Point[
100];
for (
int i =
0; i <
100; i++) points[i] =
new Point(i, i);
}
}
An alternative is to make Point
a struct.
C#Copy
struct Point
{
public
int x, y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
Now, only one object is instantiated—the one for the array—and the Point
instances are stored in-line in the array.
Struct constructors are invoked with the new
operator, but that does not imply that memory is being allocated. Instead of dynamically allocating an object and returning a reference to it, a struct constructor simply returns the struct value itself (typically in a temporary location on the stack), and this value is then copied as necessary.
With classes, it is possible for two variables to reference the same object and thus possible for operations on one variable to affect the object referenced by the other variable. With structs, the variables each have their own copy of the data, and it is not possible for operations on one to affect the other. For example, the output produced by the following code fragment depends on whether Point
is a class or a struct.
C#Copy
Point a =
new Point(
10,
10);
Point b = a;
a.x =
20;
Console.WriteLine(b.x);
If Point
is a class, the output is 20
because a
and b
reference the same object. If Point
is a struct, the output is 10
because the assignment of a
to b
creates a copy of the value, and this copy is unaffected by the subsequent assignment to a.x
.
The previous example highlights two of the limitations of structs. First, copying an entire struct is typically less efficient than copying an object reference, so assignment and value parameter passing can be more expensive with structs than with reference types. Second, except for ref
and out
parameters, it is not possible to create references to structs, which rules out their usage in a number of situations.
Arrays
An array is a data structure that contains a number of variables that are accessed through computed indices. The variables contained in an array, also called the elements of the array, are all of the same type, and this type is called the element type of the array.
Array types are reference types, and the declaration of an array variable simply sets aside space for a reference to an array instance. Actual array instances are created dynamically at run-time using the new
operator. The new
operation specifies the length of the new array instance, which is then fixed for the lifetime of the instance. The indices of the elements of an array range from 0
to Length - 1
. The new
operator automatically initializes the elements of an array to their default value, which, for example, is zero for all numeric types and null
for all reference types.
The following example creates an array of int
elements, initializes the array, and prints out the contents of the array.
C#Copy
using System;
class
Test{
static void Main() {
int[] a =
new
int[
10];
for (
int i =
0; i < a.Length; i++) {
a[i] = i * i;
}
for (
int i =
0; i < a.Length; i++) {
Console.WriteLine(
"a[{0}] = {1}", i, a[i]);
}
}
}
This example creates and operates on a single-dimensional array. C# also supports multi-dimensional arrays. The number of dimensions of an array type, also known as the rank of the array type, is one plus the number of commas written between the square brackets of the array type. The following example allocates a one-dimensional, a two-dimensional, and a three-dimensional array.
C#Copy
int[] a1 =
new
int[
10];
int[,] a2 =
new
int[
10,
5];
int[,,] a3 =
new
int[
10,
5,
2];
The a1
array contains 10 elements, the a2
array contains 50 (10 × 5) elements, and the a3
array contains 100 (10 × 5 × 2) elements.
The element type of an array can be any type, including an array type. An array with elements of an array type is sometimes called a jagged array because the lengths of the element arrays do not all have to be the same. The following example allocates an array of arrays of int
:
C#Copy
int[][] a =
new
int[
3][];
a[
0] =
new
int[
10];
a[
1] =
new
int[
5];
a[
2] =
new
int[
20];
The first line creates an array with three elements, each of type int[]
and each with an initial value of null
. The subsequent lines then initialize the three elements with references to individual array instances of varying lengths.
The new
operator permits the initial values of the array elements to be specified using an array initializer, which is a list of expressions written between the delimiters {
and }
. The following example allocates and initializes an int[]
with three elements.
C#Copy
int[] a =
new
int[] {
1,
2,
3};
Note that the length of the array is inferred from the number of expressions between {
and }
. Local variable and field declarations can be shortened further such that the array type does not have to be restated.
C#Copy
int[] a = {
1,
2,
3};
Both of the previous examples are equivalent to the following:
C#Copy
int[] t =
new
int[
3];
t[
0] =
1;
t[
1] =
2;
t[
2] =
3;
int[] a = t;
Interfaces
An interface defines a contract that can be implemented by classes and structs. An interface can contain methods, properties, events, and indexers. An interface does not provide implementations of the members it defines—it merely specifies the members that must be supplied by classes or structs that implement the interface.
Interfaces may employ multiple inheritance. In the following example, the interface IComboBox
inherits from both ITextBox
and IListBox
.
C#Copy
interface
IControl{
void Paint();
}
interface
ITextBox:
IControl{
void SetText(string text);
}
interface
IListBox:
IControl{
void SetItems(string[] items);
}
interface
IComboBox:
ITextBox,
IListBox {}
Classes and structs can implement multiple interfaces. In the following example, the class EditBox
implements both IControl
and IDataBound
.
C#Copy
interface
IDataBound{
void Bind(Binder b);
}
public
class
EditBox:
IControl,
IDataBound{
public void Paint() {...}
public void Bind(Binder b) {...}
}
When a class or struct implements a particular interface, instances of that class or struct can be implicitly converted to that interface type. For example
C#Copy
EditBox editBox =
new EditBox();
IControl control = editBox;
IDataBound dataBound = editBox;
In cases where an instance is not statically known to implement a particular interface, dynamic type casts can be used. For example, the following statements use dynamic type casts to obtain an object's IControl
and IDataBound
interface implementations. Because the actual type of the object is EditBox
, the casts succeed.
C#Copy
object obj =
new EditBox();
IControl control = (IControl)obj;
IDataBound dataBound = (IDataBound)obj;
In the previous EditBox
class, the Paint
method from the IControl
interface and the Bind
method from the IDataBound
interface are implemented using public
members. C# also supports explicit interface member implementations, using which the class or struct can avoid making the members public
. An explicit interface member implementation is written using the fully qualified interface member name. For example, the EditBox
class could implement the IControl.Paint
and IDataBound.Bind
methods using explicit interface member implementations as follows.
C#Copy
public
class
EditBox:
IControl,
IDataBound{
void IControl.Paint() {...}
void IDataBound.Bind(Binder b) {...}
}
Explicit interface members can only be accessed via the interface type. For example, the implementation of IControl.Paint
provided by the previous EditBox
class can only be invoked by first converting the EditBox
reference to the IControl
interface type.
C#Copy
EditBox editBox =
new EditBox();
editBox.Paint();
// Error, no such method
IControl control = editBox;
control.Paint();
// Ok
Enums
An enum type is a distinct value type with a set of named constants. The following example declares and uses an enum type named Color
with three constant values, Red
, Green
, and Blue
.
C#Copy
using System;
enum Color
{
Red,
Green,
Blue
}
class
Test{
static void PrintColor(Color color) {
switch (color) {
case Color.Red:
Console.WriteLine(
"Red");
break;
case Color.Green:
Console.WriteLine(
"Green");
break;
case Color.Blue:
Console.WriteLine(
"Blue");
break;
default:
Console.WriteLine(
"Unknown color");
break;
}
}
static void Main() {
Color c = Color.Red;
PrintColor(c);
PrintColor(Color.Blue);
}
}
Each enum type has a corresponding integral type called the underlying type of the enum type. An enum type that does not explicitly declare an underlying type has an underlying type of int
. An enum type's storage format and range of possible values are determined by its underlying type. The set of values that an enum type can take on is not limited by its enum members. In particular, any value of the underlying type of an enum can be cast to the enum type and is a distinct valid value of that enum type.
The following example declares an enum type named Alignment
with an underlying type of sbyte
.
C#Copy
enum Alignment:
sbyte{
Left =
-1,
Center =
0,
Right =
1
}
As shown by the previous example, an enum member declaration can include a constant expression that specifies the value of the member. The constant value for each enum member must be in the range of the underlying type of the enum. When an enum member declaration does not explicitly specify a value, the member is given the value zero (if it is the first member in the enum type) or the value of the textually preceding enum member plus one.
Enum values can be converted to integral values and vice versa using type casts. For example
C#Copy
int i = (
int)Color.Blue;
// int i = 2;
Color c = (Color)
2;
// Color c = Color.Blue;
The default value of any enum type is the integral value zero converted to the enum type. In cases where variables are automatically initialized to a default value, this is the value given to variables of enum types. In order for the default value of an enum type to be easily available, the literal 0
implicitly converts to any enum type. Thus, the following is permitted.
C#Copy
Color c =
0;
Delegates
A delegate type represents references to methods with a particular parameter list and return type. Delegates make it possible to treat methods as entities that can be assigned to variables and passed as parameters. Delegates are similar to the concept of function pointers found in some other languages, but unlike function pointers, delegates are object-oriented and type-safe.
The following example declares and uses a delegate type named Function
.
C#Copy
using System;
delegate double Function(double x);
class
Multiplier{
double factor;
public Multiplier(double factor) {
this.factor = factor;
}
public double Multiply(double x) {
return x * factor;
}
}
class
Test{
static double Square(double x) {
return x * x;
}
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 Main() {
double[] a = {
0.0,
0.5,
1.0};
double[] squares = Apply(a, Square);
double[] sines = Apply(a, Math.Sin);
Multiplier m =
new Multiplier(
2.0);
double[] doubles = Apply(a, m.Multiply);
}
}
An instance of the Function
delegate type can reference any method that takes a double
argument and returns a double
value. The Apply
method applies a given Function
to the elements of a double[]
, returning a double[]
with the results. In the Main
method, Apply
is used to apply three different functions to a double[]
.
A delegate can reference either a static method (such as Square
or Math.Sin
in the previous example) or an instance method (such as m.Multiply
in the previous example). A delegate that references an instance method also references a particular object, and when the instance method is invoked through the delegate, that object becomes this
in the invocation.
Delegates can also be created using anonymous functions, which are "inline methods" that are created on the fly. Anonymous functions can see the local variables of the surrounding methods. Thus, the multiplier example above can be written more easily without using a Multiplier
class:
C#Copy
double[] doubles = Apply(a, (
double x) => x *
2.0);
An interesting and useful property of a delegate is that it does not know or care about the class of the method it references; all that matters is that the referenced method has the same parameters and return type as the delegate.
Attributes
Types, members, and other entities in a C# program support modifiers that control certain aspects of their behavior. For example, the accessibility of a method is controlled using the public
, protected
, internal
, and private
modifiers. C# generalizes this capability such that user-defined types of declarative information can be attached to program entities and retrieved at run-time. Programs specify this additional declarative information by defining and using attributes.
The following example declares a HelpAttribute
attribute that can be placed on program entities to provide links to their associated documentation.
C#Copy
using System;
public
class
HelpAttribute:
Attribute{
string url;
string topic;
public HelpAttribute(string url) {
this.url = url;
}
public
string Url {
get {
return url; }
}
public
string Topic {
get {
return topic; }
set { topic =
value; }
}
}
All attribute classes derive from the System.Attribute
base class provided by the .NET Framework. Attributes can be applied by giving their name, along with any arguments, inside square brackets just before the associated declaration. If an attribute's name ends in Attribute
, that part of the name can be omitted when the attribute is referenced. For example, the HelpAttribute
attribute can be used as follows.
C#Copy
[
Help("http://msdn.microsoft.com/.../MyClass.htm")]
public
class
Widget{
[
Help("http://msdn.microsoft.com/.../MyClass.htm", Topic = "Display")]
public void Display(string text) {}
}
This example attaches a HelpAttribute
to the Widget
class and another HelpAttribute
to the Display
method in the class. The public constructors of an attribute class control the information that must be provided when the attribute is attached to a program entity. Additional information can be provided by referencing public read-write properties of the attribute class (such as the reference to the Topic
property previously).
The following example shows how attribute information for a given program entity can be retrieved at run-time using reflection.
C#Copy
using System;
using System.Reflection;
class
Test{
static void ShowHelp(MemberInfo member) {
HelpAttribute a = Attribute.GetCustomAttribute(member,
typeof(HelpAttribute))
as HelpAttribute;
if (a ==
null) {
Console.WriteLine(
"No help for {0}", member);
}
else {
Console.WriteLine(
"Help for {0}:", member);
Console.WriteLine(
" Url={0}, Topic={1}", a.Url, a.Topic);
}
}
static void Main() {
ShowHelp(
typeof(Widget));
ShowHelp(
typeof(Widget).GetMethod(
"Display"));
}
}
When a particular attribute is requested through reflection, the constructor for the attribute class is invoked with the information provided in the program source, and the resulting attribute instance is returned. If additional information was provided through properties, those properties are set to the given values before the attribute instance is returned.