Microsoft C# Language Specifications

Following is chapter 3 from Microsoft C# Language Specifications, a book published by Microsoft Press.


3.  Basic concepts


3.1  Program Startup

Program startup occurs when the execution environment calls a designated method, which is referred to as the programs entry point. This entry point method is always named Main, and can have one of the following signatures:

static void Main() {}

static void Main(string[] args) {}

static int Main() {}

static int Main(string[] args) {}

 

As shown, the entry point may optionally return an int value. This return value is used in program termination (section 3.2).

The entry point may optionally have one formal parameter, and this formal parameter may have any name. If such a parameter is declared, it must obey the following constraints:

   The value of this parameter must not be null.

   Let args be the name of the parameter. If the length of the array designated by args is greater than zero, the array members args[0] through args[args.Length-1], inclusive, must refer to strings, called program parameters, which are given implementation-defined values by the host environment prior to program startup. The intent is to supply to the program information determined prior to program startup from elsewhere in the hosted environment. If the host environment is not capable of supplying strings with letters in both uppercase and lowercase, the implementation shall ensure that the strings are received in lowercase.

 

Since C# supports method overloading, a class or struct may contain multiple definitions of some method, provided each has a different signature. However, within a single program, no class or struct shall contain more than one method called Main whose definition qualifies it to be used as a program entry point. Other overloaded versions of Main are permitted, provided they have more than one parameter, or their only parameter is other than type string[].

A program can be made up of multiple classes or structs, two or more of which contain a method called Main whose definition qualifies it to be used as a program entry point. In such cases, one of these Main methods must be chosen as the entry point so that program startup can occur. This choice of an entry point is beyond the scope of this specificationno mechanism for specifying or determining an entry point is provided.

 

In C#, every method must be defined as a member of a class or struct. Ordinarily, the declared accessibility (section 3.5.1) of a method is determined by the access modifiers (section 10.2.3) specified in its declaration, and similarly the declared accessibility of a type is determined by the access modifiers specified in its declaration. In order for a given method of a given type to be callable, both the type and the member must be accessible. However, the program entry point is a special case. Specifically, the execution environment can access the programs entry point regardless of its declared accessibility and regardless of the declared accessibility of its enclosing type declarations.

In all other respects, entry point methods behave like those that are not entry points.

3.2  Program Termination

Program termination returns control to the execution environment.

If the return type of the programs entry point method is int, the value returned serves as the program's termination status code. The purpose of this code is to allow communication of success or failure to the execution environment.

If the return type of the entry point method is void, reaching the right brace (}) which terminates that method, or executing a return statement that has no expression, results in a termination status code of 0.

Prior to a programs termination, finalizers for all of its objects that have not yet been finalized are called, unless such finalization has been suppressed. (The means by which a finalizer can be suppressed is outside the scope of this specification.)

3.3  Declarations

Declarations in a C# program define the constituent elements of the program. C# programs are organized using namespaces (section 9), which can contain type declarations and nested namespace declarations. Type declarations (section 9.5) are used to define classes (section 10), structs (section 11), interfaces (section 13), enums (section 14), and delegates (section 15). The kinds of members permitted in a type declaration depends on the form of the type declaration. For instance, class declarations can contain declarations for instance constructors (section 10.10), destructors (section 10.12), static constructors (section 10.11), constants (section 10.3), fields (section 10.4), methods (section 10.5), properties (section 10.6), events (section 10.7), indexers (section 10.8), operators (section 10.9), and nested types.

A declaration defines a name in the declaration space to which the declaration belongs. Except for overloaded constructor, method, indexer, and operator names, it is an error to have two or more declarations that introduce members with the same name in a declaration space. It is never possible for a declaration space to contain different kinds of members with the same name. For example, a declaration space can never contain a field and a method by the same name.

 

There are several different types of declaration spaces, as described in the following.

   Within all source files of a program, namespace-member-declarations with no enclosing namespace-declaration are members of a single combined declaration space called the global declaration space.

   Within all source files of a program, namespace-member-declarations within namespace-declarations that have the same fully qualified namespace name are members of a single combined declaration space.

   Each class, struct, or interface declaration creates a new declaration space. Names are introduced into this declaration space through class-member-declarations, struct-member-declarations, or interface-member-declarations. Except for overloaded constructor declarations and static constructor declarations, a class or struct member declaration cannot introduce a member by the same name as the class or struct. A class, struct, or interface permits the declaration of overloaded methods and indexers. A class or struct furthermore permits the declaration of overloaded constructors and operators. For instance, a class, struct, or interface may contain multiple method declarations with the same name, provided these method declarations differ in their signature (section 3.6). Note that base classes do not contribute to the declaration space of a class, and base interfaces do not contribute to the declaration space of an interface. Thus, a derived class or interface is allowed to declare a member with the same name as an inherited member. Such a member is said to hide the inherited member.

   Each enumeration declaration creates a new declaration space. Names are introduced into this declaration space through enum-member-declarations.

   Each block or switch-block creates a separate declaration space for local variables. Names are introduced into this declaration space through local-variable-declarations. If a block is the body of a constructor or method declaration, the parameters declared in the formal-parameter-list are members of the blocks local variable declaration space. The local variable declaration space of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a local variable with the same name as a local variable in an enclosing block.

   Each block or switch-block creates a separate declaration space for labels. Names are introduced into this declaration space through labeled-statements, and the names are referenced through goto-statements. The label declaration space of a block includes any nested blocks. Thus, within a nested block it is not possible to declare a label with the same name as a label in an enclosing block.

 

 

The textual order in which names are declared is generally of no significance. In particular, textual order is not significant for the declaration and use of namespaces, types, constants, methods, properties, events, indexers, operators, constructors, destructors, and static constructors. Declaration order is significant in the following ways:

   Declaration order for field declarations and local variable declarations determines the order in which their initializers (if any) are executed.

   Local variables must be defined before they are used (section 3.7).

   Declaration order for enum member declarations (section 14.2) is significant when constant-expression values are omitted.

 

The declaration space of a namespace is open ended, and two namespace declarations with the same fully qualified name contribute to the same declaration space. For example

namespace Megacorp.Data
{
   class Customer
   {
    
   }
}

namespace Megacorp.Data
{
   class Order
   {
    
   }
}

 

The two namespace declarations above contribute to the same declaration space, in this case declaring two classes with the fully qualified names Megacorp.Data.Customer and Megacorp.Data.Order. Because the two declarations contribute to the same declaration space, it would have been an error if each contained a declaration of a class with the same name.

The declaration space of a block includes any nested blocks. Thus, in the following example, the F and G methods are in error because the name i is declared in the outer block and cannot be redeclared in the inner block. However, the H and I methods are valid since the two is are declared in separate non-nested blocks.

 

class A
{
   void F() {
     int i = 0;
     if (true) {
        int i = 1;      
     }
   }

void G() {
     if (true) {
        int i = 0;
     }
     int i = 1;        
   }

void H() {
     if (true) {
        int i = 0;
     }
     if (true) {
        int i = 1;
     }
   }

void I() {
     for (int i = 0; i < 10; i++)
        H();
     for (int i = 0; i < 10; i++)
        H();
   }
}

 

3.4  Members

Namespaces and types have members. The members of an entity are generally available through the use of a qualified name that starts with a reference to the entity, followed by a . token, followed by the name of the member.

Members of a type are either declared in the type or inherited from the base class of the type. When a type inherits from a base class, all members of the base class, except constructors and destructors, become members of the derived type. The declared accessibility of a base class member does not control whether the member is inheritedinheritance extends to any member that isnt a constructor or destructor. However, an inherited member may not be accessible in a derived type, either because of its declared accessibility (section 3.5.1) or because it is hidden by a declaration in the type itself (section 3.7.1.2).

 

3.4.1  Namespace members

Namespaces and types that have no enclosing namespace are members of the global namespace. This corresponds directly to the names declared in the global declaration space.

Namespaces and types declared within a namespace are members of that namespace. This corresponds directly to the names declared in the declaration space of the namespace.

Namespaces have no access restrictions. It is not possible to declare private, protected, or internal namespaces, and namespace names are always publicly accessible.

3.4.2  Struct members

The members of a struct are the members declared in the struct and the members inherited from class object.

The members of a simple type correspond directly to the members of the struct type aliased by the simple type:

   The members of sbyte are the members of the System.SByte struct.

   The members of byte are the members of the System.Byte struct.

   The members of short are the members of the System.Int16 struct.

   The members of ushort are the members of the System.UInt16 struct.

   The members of int are the members of the System.Int32 struct.

   The members of uint are the members of the System.UInt32 struct.

   The members of long are the members of the System.Int64 struct.

   The members of ulong are the members of the System.UInt64 struct.

   The members of char are the members of the System.Char struct.

   The members of float are the members of the System.Single struct.

   The members of double are the members of the System.Double struct.

   The members of decimal are the members of the System.Decimal struct.

   The members of bool are the members of the System.Boolean struct.

 

3.4.3  Enumeration members

The members of an enumeration are the constants declared in the enumeration and the members inherited from class object.

 

3.4.4  Class members

The members of a class are the members declared in the class and the members inherited from the base class (except for class object which has no base class). The members inherited from the base class include the constants, fields, methods, properties, events, indexers, operators, and types of the base class, but not the constructors, destructors, and static constructors of the base class. Base class members are inherited without regard to their accessibility.

A class declaration may contain declarations of constants, fields, methods, properties, events, indexers, operators, constructors, destructors, static constructors, and types.

The members of object and string correspond directly to the members of the class types they alias:

   The members of object are the members of the System.Object class.

   The members of string are the members of the System.String class.

 

3.4.5  Interface members

The members of an interface are the members declared in the interface and in all base interfaces of the interface, and the members inherited from class object.

3.4.6  Array members

The members of an array are the members inherited from class System.Array.

3.4.7  Delegate members

The members of a delegate are the members inherited from class System.Delegate.

3.5  Member access

Declarations of members allow control over member access. The accessibility of a member is established by the declared accessibility (section 3.5.1) of the member combined with the accessibility of the immediately containing type, if any.

When access to a particular member is allowed, the member is said to be accessible. Conversely, when access to a particular member is disallowed, the member is said to be inaccessible. Access to a member is permitted when the textual location in which the access takes place is included in the accessibility domain (section 3.5.2) of the member.

 

3.5.1  Declared accessibility

The declared accessibility of a member can be one of the following:

   Public, which is selected by including a public modifier in the member declaration. The intuitive meaning of public is access not limited.

   Protected internal (meaning protected or internal), which is selected by including both a protected and an internal modifier in the member declaration. The intuitive meaning of protected internal is access limited to this program or types derived from the containing class.

   Protected, which is selected by including a protected modifier in the member declaration. The intuitive meaning of protected is access limited to the containing class or types derived from the containing class.

   Internal, which is selected by including an internal modifier in the member declaration. The intuitive meaning of internal is access limited to this program.

   Private, which is selected by including a private modifier in the member declaration. The intuitive meaning of private is access limited to the containing type.

 

Depending on the context in which a member declaration takes place, only certain types of declared accessibility are permitted. Furthermore, when a member declaration does not include any access modifiers, the context in which the declaration takes place determines the default declared accessibility.

   Namespaces implicitly have public declared accessibility. No access modifiers are allowed on namespace declarations.

   Types declared in compilation units or namespaces can have public or internal declared accessibility and default to internal declared accessibility.

   Class members can have any of the five types of declared accessibility and default to private declared accessibility. (Note that a type declared as a member of a class can have any of the five types of declared accessibility, whereas a type declared as a member of a namespace can have only public or internal declared accessibility.)

   Struct members can have public, internal, or private declared accessibility and default to private declared accessibility. Struct members cannot have protected or protected internal declared accessibility.

   Interface members implicitly have public declared accessibility. No access modifiers are allowed on interface member declarations.

   Enumeration members implicitly have public declared accessibility. No access modifiers are allowed on enumeration member declarations.

 

 

3.5.2  Accessibility domains

The accessibility domain of a member is the (possibly disjoint) sections of program text in which access to the member is permitted. For purposes of defining the accessibility domain of a member, a member is said to be top-level if it is not declared within a type, and a member is said to be nested if it is declared within another type. Furthermore, the program text of a program is defined as all program text contained in all source files of the program, and the program text of a type is defined as all program text contained between the opening and closing { and } tokens in the class-body, struct-body, interface-body, or enum-body of the type (including, possibly, types that are nested within the type).

The accessibility domain of a predefined type (such as object, int, or double) is unlimited.

The accessibility domain of a top-level type T declared in a program P is defined as follows:

   If the declared accessibility of T is public, the accessibility domain of T is the program text of P and any program that references P.

   If the declared accessibility of T is internal, the accessibility domain of T is the program text of P.

 

From these definitions it follows that the accessibility domain of a top-level type is always at least the program text of the program in which the type is declared.

The accessibility domain of a nested member M declared in a type T within a program P is defined as follows (noting that M may itself possibly be a type):

   If the declared accessibility of M is public, the accessibility domain of M is the accessibility domain of T.

   If the declared accessibility of M is protected internal, the accessibility domain of M is the intersection of the accessibility domain of T with the program text of P and the program text of any type derived from T declared outside P.

   If the declared accessibility of M is protected, the accessibility domain of M is the intersection of the accessibility domain of T with the program text of T and any type derived from T.

   If the declared accessibility of M is internal, the accessibility domain of M is the intersection of the accessibility domain of T with the program text of P.

   If the declared accessibility of M is private, the accessibility domain of M is the program text of T.

 

From these definitions it follows that the accessibility domain of a nested member is always at least the program text of the type in which the member is declared. Furthermore, it follows that the accessibility domain of a member is never more inclusive than the accessibility domain of the type in which the member is declared.

 

In intuitive terms, when a type or member M is accessed, the following steps are evaluated to ensure that the access is permitted:

   First, if M is declared within a type (as opposed to a compilation unit or a namespace), an error occurs if that type is not accessible.

   Then, if M is public, the access is permitted.

   Otherwise, if M is protected internal, the access is permitted if it occurs within the program in which M is declared, or if it occurs within a class derived from the class in which M is declared and takes place through the derived class type (section 3.5.3).

   Otherwise, if M is protected, the access is permitted if it occurs within the class in which M is declared, or if it occurs within a class derived from the class in which M is declared and takes place through the derived class type (section 3.5.3).

   Otherwise, if M is internal, the access is permitted if it occurs within the program in which M is declared.

   Otherwise, if M is private, the access is permitted if it occurs within the type in which M is declared.

   Otherwise, the type or member is inaccessible, and an error occurs.

 

In the example

public class A
{
   public static int X;
   internal static int Y;
   private static int Z;
}

internal class B
{
   public static int X;
   internal static int Y;
   private static int Z;

   public class C
   {
     public static int X;
     internal static int Y;
     private static int Z;
   }

   private class D
   {
     public static int X;
     internal static int Y;
     private static int Z;
   }
}

 

 

the classes and members have the following accessibility domains:

   The accessibility domain of A and A.X is unlimited.

   The accessibility domain of A.Y, B, B.X, B.Y, B.C, B.C.X, and B.C.Y is the program text of the containing program.

   The accessibility domain of A.Z is the program text of A.

   The accessibility domain of B.Z and B.D is the program text of B, including the program text of B.C and B.D.

   The accessibility domain of B.C.Z is the program text of B.C.

   The accessibility domain of B.D.X, B.D.Y, and B.D.Z is the program text of B.D.

 

As the example illustrates, the accessibility domain of a member is never larger than that of a containing type. For example, even though all X members have public declared accessibility, all but A.X have accessibility domains that are constrained by a containing type.

As described in section 3.4, all members of a base class, except for constructors and destructors, are inherited by derived types. This includes even private members of a base class. However, the accessibility domain of a private member includes only the program text of the type in which the member is declared. In the example

class A
{
   int x;

   static void F(B b) {
     b.x = 1;     // Ok
   }
}

class B: A
{
   static void F(B b) {
     b.x = 1;     // Error, x not accessible
   }
}

 

the B class inherits the private member x from the A class. Because the member is private, it is only accessible within the class-body of A. Thus, the access to b.x succeeds in the A.F method, but fails in the B.F method.

 

3.5.3  Protected access

When a protected member is accessed outside the program text of the class in which it is declared, and when a protected internal member is accessed outside the program text of the program in which it is declared, the access is required to take place through the derived class type in which the access occurs. Let B be a base class that declares a protected member M, and let D be a class that derives from B. Within the class-body of D, access to M can take one of the following forms:

   An unqualified type-name or primary-expression of the form M.

   A type-name of the form T.M, provided T is D or a class derived from D.

   A primary-expression of the form E.M, provided the type of E is D or a class derived from D.

   A primary-expression of the form base.M.

 

In addition to these forms of access, a derived class can access a protected constructor of a base class in a constructor-initializer (section 10.10.1).

In the example

public class A
{
   protected int x;

   static void F(A a, B b) {
     a.x = 1;     // Ok
     b.x = 1;     // Ok
   }
}

public class B: A
{
   static void F(A a, B b) {
     a.x = 1;     // Error, must access through instance of B
     b.x = 1;     // Ok
   }
}

 

within A, it is possible to access x through instances of both A and B, since in either case the access takes place through an instance of A or a class derived from A. However, within B, it is not possible to access x through an instance of A, since A does not derive from B.

 

3.5.4  Accessibility constraints

Several constructs in the C# language require a type to be at least as accessible as a member or another type. A type T is said to be at least as accessible as a member or type M if the accessibility domain of T is a superset of the accessibility domain of M. In other words, T is at least as accessible as M if T is accessible in all contexts where M is accessible.

The following accessibility constraints exist:

   The direct base class of a class type must be at least as accessible as the class type itself.

   The explicit base interfaces of an interface type must be at least as accessible as the interface type itself.

   The return type and parameter types of a delegate type must be at least as accessible as the delegate type itself.

   The type of a constant must be at least as accessible as the constant itself.

   The type of a field must be at least as accessible as the field itself.

   The return type and parameter types of a method must be at least as accessible as the method itself.

   The type of a property must be at least as accessible as the property itself.

   The type of an event must be at least as accessible as the event itself.

   The type and parameter types of an indexer must be at least as accessible as the indexer itself.

   The return type and parameter types of an operator must be at least as accessible as the operator itself.

   The parameter types of a constructor must be at least as accessible as the constructor itself.

 

In the example

class A {}

public class B: A {}

 

the B class is in error because A is not at least as accessible as B.

 

Likewise, in the example

class A {}

public class B
{
   A F() {}

   internal A G() {}

   public A H() {}
}

 

the H method in B is in error because the return type A is not at least as accessible as the method.

3.6  Signatures and overloading

Methods, constructors, indexers, and operators are characterized by their signatures:

   The signature of a method consists of the name of the method and the type and kind (value, reference, or output) of each of its formal parameters. The signature of a method specifically does not include the return type, nor does it include the params modifier that may be specified for the last parameter.

   The signature of a constructor consists of the type and kind (value, reference, or output) of each of its formal parameters. The signature of a constructor specifically does not include the params modifier that may be specified for the last parameter.

   The signature of an indexer consists of the type of each of its formal parameters. The signature of an indexer specifically does not include the element type.

   The signature of an operator consists of the name of the operator and the type of each of its formal parameters. The signature of an operator specifically does not include the result type.

 

Signatures are the enabling mechanism for overloading of members in classes, structs, and interfaces:

   Overloading of methods permits a class, struct, or interface to declare multiple methods with the same name, provided the signatures of the methods are all unique.

   Overloading of constructors permits a class or struct to declare multiple constructors, provided the signatures of the constructors are all unique.

   Overloading of indexers permits a class, struct, or interface to declare multiple indexers, provided the signatures of the indexers are all unique.

   Overloading of operators permits a class or struct to declare multiple operators with the same name, provided the signatures of the operators are all unique.

 

 

The following example shows a set of overloaded method declarations along with their signatures.

interface ITest
{
   void F();                   // F()

   void F(int x);                // F(int)

   void F(ref int x);          // F(ref int)

   void F(out int x);          // F(out int)

   void F(int x, int y);       // F(int, int)

   int F(string s);             // F(string)

   int F(int x);                // F(int)

   void F(string[] a);           // F(string[])

   void F(params string[] a);     // F(string[])
}

 

Note that the ref and out parameter modifiers (section 10.5.1) are part of a signature. Thus, F(int), F(ref int), and F(out int) are all unique signatures. Also note that the return type and the params modifier are not part of a signature, and that it is not possible to overload solely based on return type or solely based on the inclusion or exclusion of the params modifier. Because of these restrictions, compiling the above example would produce errors for the methods with the duplicate signatures F(int) and F(string[]).

3.7  Scopes

The scope of a name is the region of program text within which it is possible to refer to the entity declared by the name without qualification of the name. Scopes can be nested, and an inner scope may redeclare the meaning of a name from an outer scope. The name from the outer scope is then said to be hidden in the region of program text covered by the inner scope, and access to the outer name is only possible by qualifying the name.

   The scope of a namespace member declared by a namespace-member-declaration with no enclosing namespace-declaration is the entire program text of each compilation unit.

   The scope of a namespace member declared by a namespace-member-declaration within a namespace-declaration whose fully qualified name is N is the namespace-body of every namespace-declaration whose fully qualified name is N or starts with the same sequence of identifiers as N.

   The scope of a name defined or imported by a using-directive extends over the namespace-member-declarations of the compilation-unit or namespace-body in which the using-directive occurs. A using-directive may make zero or more namespace or type names available within a particular compilation-unit or namespace-body, but does not contribute any new members to the underlying declaration space. In other words, a using-directive is not transitive but rather affects only the compilation-unit or namespace-body in which it occurs.

 

   The scope of a member declared by a class-member-declaration is the class-body in which the declaration occurs. In addition, the scope of a class member extends to the class-body of those derived classes that are included in the accessibility domain (section 3.5.2) of the member.

   The scope of a member declared by a struct-member-declaration is the struct-body in which the declaration occurs.

   The scope of a member declared by an enum-member-declaration is the enum-body in which the declaration occurs.

   The scope of a parameter declared in a constructor-declaration is the constructor-initializer and block of that constructor-declaration.

   The scope of a parameter declared in a method-declaration is the method-body of that method-declaration.

   The scope of a parameter declared in an indexer-declaration is the accessor-declarations of that indexer-declaration.

   The scope of a parameter declared in an operator-declaration is the block of that operator-declaration.

   The scope of a local variable declared in a local-variable-declaration is the block in which the declaration occurs. It is an error to refer to a local variable in a textual position that precedes the variable-declarator of the local variable.

   The scope of a local variable declared in a for-initializer of a for statement is the for-initializer, the for-condition, the for-iterator, and the contained statement of the for statement.

   The scope of a label declared in a labeled-statement is the block in which the declaration occurs.

 

Within the scope of a namespace, class, struct, or enumeration member it is possible to refer to the member in a textual position that precedes the declaration of the member. For example

class A
{
   void F() {
     i = 1;
   }

   int i = 0;
}

 

Here, it is valid for F to refer to i before it is declared.

Within the scope of a local variable, it is an error to refer to the local variable in a textual position that precedes the variable-declarator of the local variable. For example

 

class A
{
   int i = 0;

   void F() {
     i = 1;             // Error, use precedes declaration
     int i;
     i = 2;
   }

   void G() {
     int j = (j = 1);      // Legal
   }

   void H() {
     int a = 1, b = ++a;   // Legal
   }
}

 

In the F method above, the first assignment to i specifically does not refer to the field declared in the outer scope. Rather, it refers to the local variable and it is in error because it textually precedes the declaration of the variable. In the G method, the use of j in the initializer for the declaration of j is legal because the use does not precede the variable-declarator. In the H method, a subsequent variable-declarator legally refers to a local variable declared in an earlier variable-declarator within the same local-variable-declaration.

The scoping rules for local variables are designed to guarantee that the meaning of a name used in an expression context is always the same within a block. If the scope of a local variable was to extend only from its declaration to the end of the block, then in the example above, the first assignment would assign to the instance variable and the second assignment would assign to the local variable, possibly leading to errors if the statements of the block were later to be rearranged.

The meaning of a name within a block may differ based on the context in which the name is used. In the example

class Test
{
   static void Main() {
     string A = "hello, world";
     string s = A;                     // expression context

     Type t = typeof(A);               // type context

      Console.WriteLine(s);            // writes "hello, world"
      Console.WriteLine(t.ToString());      // writes "Type: A"
   }
}

 

the name A is used in an expression context to refer to the local variable A and in a type context to refer to the class A.

 

3.7.1  Name hiding

The scope of an entity typically encompasses more program text than the declaration space of the entity. In particular, the scope of an entity may include declarations that introduce new declaration spaces containing entities of the same name. Such declarations cause the original entity to become hidden. Conversely, an entity is said to be visible when it is not hidden.

Name hiding occurs when scopes overlap through nesting and when scopes overlap through inheritance. The characteristics of the two types of hiding are described in the following sections.

3.7.1.1  Hiding through nesting

Name hiding through nesting can occur as a result of nesting namespaces or types within namespaces, as a result of nesting types within classes or structs, and as a result of parameter and local variable declarations. Name hiding through nesting of scopes always occurs silently, i.e., no errors or warnings are reported when outer names are hidden by inner names.

In the example

class A
{
   int i = 0;

   void F() {
     int i = 1;
   }

   void G() {
     i = 1;
   }
}

 

within the F method, the instance variable i is hidden by the local variable i, but within the G method, i still refers to the instance variable.

When a name in an inner scope hides a name in an outer scope, it hides all overloaded occurrences of that name. In the example

class Outer
{
   static void F(int i) {}

   static void F(string s) {}

   class Inner
   {

 

     void G() {
        F(1);         // Invokes Outer.Inner.F
        F("Hello");     // Error
     }

     static void F(long l) {}
   }
}

 

the call F(1) invokes the F declared in Inner because all outer occurrences of F are hidden by the inner declaration. For the same reason, the call F("Hello") is in error.

3.7.1.2  Hiding through inheritance

Name hiding through inheritance occurs when classes or structs redeclare names that were inherited from base classes. This type of name hiding takes one of the following forms:

   A constant, field, property, event, or type introduced in a class or struct hides all base class members with the same name.

   A method introduced in a class or struct hides all non-method base class members with the same name, and all base class methods with the same signature (method name and parameter count, modifiers, and types).

   An indexer introduced in a class or struct hides all base class indexers with the same signature (parameter count and types).

 

The rules governing operator declarations (section 10.9) make it impossible for a derived class to declare an operator with the same signature as an operator in a base class. Thus, operators never hide one another.

Contrary to hiding a name from an outer scope, hiding an accessible name from an inherited scope causes a warning to be reported. In the example

class Base
{
   public void F() {}
}

class Derived: Base
{
   public void F() {}     // Warning, hiding an inherited name
}

 

 

the declaration of F in Derived causes a warning to be reported. Hiding an inherited name is specifically not an error, since that would preclude separate evolution of base classes. For example, the above situation might have come about because a later version of Base introduced an F method that wasnt present in an earlier version of the class. Had the above situation been an error, then any change made to a base class in a separately versioned class library could potentially cause derived classes to become invalid.

The warning caused by hiding an inherited name can be eliminated through use of the new modifier:

class Base
{
   public void F() {}
}

class Derived: Base
{
   new public void F() {}
}

 

The new modifier indicates that the F in Derived is new, and that it is indeed intended to hide the inherited member.

A declaration of a new member hides an inherited member only within the scope of the new member.

class Base
{
   public static void F() {}
}

class Derived: Base
{
   new private static void F() {}  // Hides Base.F in Derived only
}

class MoreDerived: Derived
{
   static void G() { F(); }       // Invokes Base.F
}

 

In the example above, the declaration of F in Derived hides the F that was inherited from Base, but since the new F in Derived has private access, its scope does not extend to MoreDerived. Thus, the call F() in MoreDerived.G is valid and will invoke Base.F.

 

3.8  Namespace and type names

Several contexts in a C# program require a namespace-name or a type-name to be specified. Either form of name is written as one or more identifiers separated by . tokens.

namespace-name:
namespace-or-type-name

type-name:
namespace-or-type-name

namespace-or-type-name:
identifier
namespace-or-type-name   .   identifier

 

A type-name is a namespace-or-type-name that refers to a type. Following resolution as described below, the namespace-or-type-name of a type-name must refer to a type, or otherwise an error occurs.

A namespace-name is a namespace-or-type-name that refers to a namespace. Following resolution as described below, the namespace-or-type-name of a namespace-name must refer to a namespace, or otherwise an error occurs.

The meaning of a namespace-or-type-name is determined as follows:

   If the namespace-or-type-name consists of a single identifier:

   If the namespace-or-type-name appears within the body of a class or struct declaration, then starting with that class or struct declaration and continuing with each enclosing class or struct declaration (if any), if a member with the given name exists, is accessible, and denotes a type, then the namespace-or-type-name refers to that member. Note that non-type members (constructors, constants, fields, methods, properties, indexers, and operators) are ignored when determining the meaning of a namespace-or-type-name.

   Otherwise, starting with the namespace declaration in which the namespace-or-type-name occurs (if any), continuing with each enclosing namespace declaration (if any), and ending with the global namespace, the following steps are evaluated until an entity is located:

   If the namespace contains a namespace member with the given name, then the namespace-or-type-name refers to that member and, depending on the member, is classified as a namespace or a type.

  Otherwise, if the namespace declaration contains a using-alias-directive that associates the given name with an imported namespace or type, then the namespace-or-type-name refers to that namespace or type.

 

  Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain exactly one type with the given name, then the namespace-or-type-name refers to that type.

  Otherwise, if the namespaces imported by the using-namespace-directives of the namespace declaration contain more than one type with the given name, then the namespace-or-type-name is ambiguous and an error occurs.

   Otherwise, the namespace-or-type-name is undefined and an error occurs.

   Otherwise, the namespace-or-type-name is of the form N.I, where N is a namespace-or-type-name consisting of all identifiers but the rightmost one, and I is the rightmost identifier. N is first resolved as a namespace-or-type-name. If the resolution of N is not successful, an error occurs. Otherwise, N.I is resolved as follows:

   If N is a namespace and I is the name of an accessible member of that namespace, then N.I refers to that member and, depending on the member, is classified as a namespace or a type.

   If N is a class or struct type and I is the name of an accessible type in N, then N.I refers to that type.

   Otherwise, N.I is an invalid namespace-or-type-name, and an error occurs.

 

3.8.1  Fully qualified names

Every namespace and type has a fully qualified name which uniquely identifies the namespace or type amongst all others. The fully qualified name of a namespace or type N is determined as follows:

   If N is a member of the global namespace, its fully qualified name is N.

   Otherwise, its fully qualified name is S.N, where S is the fully qualified name of the namespace or type in which N is declared.

 

In other words, the fully qualified name of N is the complete hierarchical path of identifiers that lead to N, starting from the global namespace. Because every member of a namespace or type must have a unique name, it follows that the fully qualified name of a namespace or type is always unique.

 

The example below shows several namespace and type declarations along with their associated fully qualified names.

class A {}         // A

namespace X           // X
{
   class B         // X.B
   {
     class C {}   // X.B.C
   }

   namespace Y        // X.Y
   {
     class D {}   // X.Y.D
   }
}

namespace X.Y      // X.Y
{
   class E {}      // X.Y.E
}

 


 

 


To purchase a copy of this book, click HERE.



Comments

Leave a Comment

Top White Papers and Webcasts

  • Live Event Date: September 10, 2014 @ 11:00 a.m. ET / 8:00 a.m. PT Modern mobile applications connect systems-of-engagement (mobile apps) with systems-of-record (traditional IT) to deliver new and innovative business value. But the lifecycle for development of mobile apps is also new and different. Emerging trends in mobile development call for faster delivery of incremental features, coupled with feedback from the users of the app "in the wild". This loop of continuous delivery and continuous feedback is …

  • Java developers know that testing code changes can be a huge pain, and waiting for an application to redeploy after a code fix can take an eternity. Wouldn't it be great if you could see your code changes immediately, fine-tune, debug, explore and deploy code without waiting for ages? In this white paper, find out how that's possible with a Java plugin that drastically changes the way you develop, test and run Java applications. Discover the advantages of this plugin, and the changes you can expect to see …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds