Variables are used to store values. More technically, a variablebinds anobject (in the general sense of the term, i.e. a specific value) to an identifier (the variable's name) so that the object can be accessed later. Variables can, for example, store a value for later use:
stringname="Dr. Jones";Console.WriteLine("Good morning "+name);
In this example "name" is the identifier and "Dr. Jones" is the value that we bound to it. Also, each variable is declared with an explicittype. Only values whose types are compatible with the variable's declared type can be bound to (stored in) the variable. In the above example we stored "Dr. Jones" into a variable of the typestring
. This is a legal statement. However, if we had saidintname
="Dr. Jones"
, the compiler would have thrown an error telling us that you cannot implicitly convert betweenint
andstring
. There are methods for doing this, but we will talk about them later.
C# supports several program elements corresponding to the general programming concept ofvariable:fields,parameters, andlocal variables.
Fields, sometimes called class-level variables, are variables associated with classes or structures. Aninstance variable is a field associated with an instance of the class or structure, while astatic variable, declared with thestatic keyword, is a field associated with the type itself. Fields can also be associated with their class by making themconstants (const), which requires a declaration assignment of a constant value and prevents subsequent changes to the field.
Each field has a visibility ofpublic,protected,internal,protected internal, orprivate (from most visible to least visible).
Like fields, local variables can optionally beconstant (const). Constant local variables are stored in the assembly data region, while non-constant local variables are stored on (or referenced from) the stack. They thus have both a scope and an extent of the method or statement block that declares them.
Parameters are variables associated with a method.
Anin parameter may either have its value passed in from the caller to the method's environment, so that changes to the parameter by the method do not affect the value of the caller's variable, or passed in by reference, so that changes to the variables will affect the value of the caller's variable. Value types (int, double, string) are passed in "by value" while reference types (objects) are passed in "by reference." Since this is the default for the C# compiler, it is not necessary to use '&', as in C or C++.
Anout parameter does not have its value copied, thus changes to the variable's value within the method's environment directly affect the value from the caller's environment. Such a variable is considered by the compiler to beunbound upon method entry, thus it is illegal to reference anout parameter before assigning it a value. It alsomust be assigned by the method in each valid (non-exceptional) code path through the method in order for the method to compile.
Areference parameter is similar to anout parameter, except that it isbound before the method call and it need not be assigned by the method.
Aparams parameter represents a variable number of parameters. If a method signature includes one, theparams argument must be the last argument in the signature.
// Each pair of lines is what the definition of a method and a call of a// method with each of the parameters types would look like.// In param:voidMethodOne(intparam1)// definitionMethodOne(variable);// call// Out param:voidMethodTwo(outstringmessage)// definitionMethodTwo(outvariable);// call// Reference param;voidMethodThree(refintsomeFlag)// definitionMethodThree(reftheFlag)// call// ParamsvoidMethodFour(paramsstring[]names)// definitionMethodFour("Matthew","Mark","Luke","John");// call
Eachtype in C# is either avalue type or areference type. C# has several predefined ("built-in") types and allows for declaration of custom value types and reference types.
There is a fundamental difference between value types and reference types: Value types are allocated on the stack, whereas reference types are allocated on the heap.
The value types in the .NET framework are usually small, frequently used types. The benefit of using them is that the type requires very little resources to get up and running by the CLR. Value types do not require memory to be allocated on the heap and therefore will not cause garbage collection. However, in order to be useful, the value types (or types derived from it) should remain small - ideally below 16 bytes of data. If you choose to make your value type bigger, it is recommended that you do not pass it to methods (which can require a copy of all its fields), or return it from methods.
Although this sounds like a useful type to have, it does have some flaws, which need to be understood when using it.
Reference types are managed very differently by the CLR. All reference types consist of two parts: A pointer to the heap (which contains the object), and the object itself. Reference types are slightly heavier weight because of the management behind the scenes needed to keep track of them. However, this is a minor price to pay for the flexibility and speed gains from passing a pointer around, rather than copying values to/from methods.
When an object is initialized, by use of the constructor, and is of a reference type, the CLR must perform four operations:
This occurs every single time an object is created. However the assumption is that there is infinite memory, therefore some maintenance needs to take place - and that's where the garbage collector comes in.
Because the type system in C# is unified with other languages that are CLI-compliant, each integral C# type is actually an alias for a corresponding type in the .NET framework. Although the names of the aliases vary between .NET languages, the underlying types in the .NET framework remain the same. Thus, objects created in assemblies written in other languages of the .NET Framework can be bound to C# variables of any type to which the value can be converted, per the conversion rules below. The following illustrates the cross-language compatibility of types by comparing C# code with the equivalent Visual Basic .NET code:
// C#publicvoidUsingCSharpTypeAlias(){inti=42;}publicvoidEquivalentCodeWithoutAlias(){System.Int32i=42;}
' Visual Basic .NETPublicSubUsingVisualBasicTypeAlias()DimiAsInteger=42EndSubPublicSubEquivalentCodeWithoutAlias()DimiAsSystem.Int32=42EndSub
Using the language-specific type aliases is often considered more readable than using the fully-qualified .NET Framework type names.
The fact that each C# type corresponds to a type in the unified type system gives eachvalue type a consistent size across platforms and compilers. That consistency is an important distinction from other languages such as C, where, e.g. along
is only guaranteed to be atleast as large as anint
, and is implemented with different sizes by different compilers. Asreference types, variables of types derived fromobject
(i.e. anyclass
) are exempt from the consistent size requirement. That is, the size ofreference types likeSystem.IntPtr
, as opposed tovalue types likeSystem.Int32
, may vary by platform. Fortunately, there is rarely a need to know the actual size of areference type.
There are two predefinedreference types:object
, an alias for theSystem.Object
class, from which all other reference types derive; andstring
, an alias for theSystem.String
class. C# likewise has several integral value types, each an alias to a corresponding value type in theSystem
namespace of the .NET Framework. The predefined C# type aliases expose the methods of the underlying .NET Framework types. For example, since the .NET Framework'sSystem.Int32
type implements aToString()
method to convert the value of an integer to its string representation, C#'sint
type exposes that method:
inti=97;strings=i.ToString();// The value of s is now the string "97".
Likewise, theSystem.Int32
type implements theParse()
method, which can therefore be accessed via C#'sint
type:
strings="97";inti=int.Parse(s);// The value of i is now the integer 97.
The unified type system is enhanced by the ability to convert value types to reference types (boxing) and likewise to convert certain reference types to their corresponding value types (unboxing). This is also known ascasting.
objectboxedInteger=97;intunboxedInteger=(int)boxedInteger;
Boxing and casting are, however, not type-safe: the compiler won't generate an error if the programmer mixes up the types. In the following short example the mistake is quite obvious, but in complex programs it may be very difficult to spot. Avoid boxing, if possible.
objectgetInteger="97";intanInteger=(int)getInteger;// No compile-time error. The program will crash, however.
The built-in C# type aliases and their equivalent .NET Framework types follow:
C# Alias | .NET Type | Size (bits) | Range |
---|---|---|---|
sbyte | System.SByte | 8 | -128 to 127 |
byte | System.Byte | 8 | 0 to 255 |
short | System.Int16 | 16 | -32,768 to 32,767 |
ushort | System.UInt16 | 16 | 0 to 65,535 |
char | System.Char | 16 | A unicode character of code 0 to 65,535 |
int | System.Int32 | 32 | -2,147,483,648 to 2,147,483,647 |
uint | System.UInt32 | 32 | 0 to 4,294,967,295 |
long | System.Int64 | 64 | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 |
ulong | System.UInt64 | 64 | 0 to 18,446,744,073,709,551,615 |
C# Alias | .NET Type | Size (bits) | Precision | Range |
---|---|---|---|---|
float | System.Single | 32 | 7 digits | 1.5 x 10-45 to 3.4 x 1038 |
double | System.Double | 64 | 15-16 digits | 5.0 x 10-324 to 1.7 x 10308 |
decimal | System.Decimal | 128 | 28-29 decimal places | 1.0 x 10-28 to 7.9 x 1028 |
C# Alias | .NET Type | Size (bits) | Range |
---|---|---|---|
bool | System.Boolean | 32 | true or false, which aren't related to any integer in C#. |
object | System.Object | 32/64 | Platform dependent (a pointer to an object). |
string | System.String | 16*length | A unicode string with no special upper bound. |
The predefined types can be aggregated and extended into custom types.
Customvalue types are declared with thestruct orenum keyword. Likewise,custom reference types are declared with theclass keyword.
Although the number of dimensions is included in array declarations, the size of each dimension is not:
string[]a_str;
Assignments to an array variable (prior to the variable's usage), however, specify the size of each dimension:
a_str=newstring[5];
As with other variable types, the declaration and the initialization can be combined:
string[]a_str=newstring[5];
It is also important to note that like in Java, arrays are passed by reference, and not passed by value. For example, the following code snippet successfully swaps two elements in an integer array:
staticvoidswap(int[]a_iArray,intiI,intiJ){intiTemp=a_iArray[iI];a_iArray[iI]=a_iArray[iJ];a_iArray[iJ]=iTemp;}
It is possible to determine the array size during runtime. The following example assigns the loop counter to the unsigned short array elements:
ushort[]a_usNumbers=newushort[234];[...]for(ushortus=0;us<a_usNumbers.Length;us++){a_usNumbers[us]=us;}
Since C# 2.0, it is possible to have arrays also inside ofstructures.
usingSystem;namespaceLogin{classUsername_Password{publicstaticvoidMain(){stringusername,password;Console.Write("Enter username: ");username=Console.ReadLine();Console.Write("Enter password: ");password=Console.ReadLine();if(username=="SomePerson"&&password=="SomePassword"){Console.WriteLine("Access Granted.");}elseif(username!="SomePerson"&&password=="SomePassword"){Console.WriteLine("The username is wrong.");}elseif(username=="SomePerson"&&password!="SomePassword"){Console.WriteLine("The password is wrong.");}else{Console.WriteLine("Access Denied.");}}}}
Values of a given type may or may not be explicitly or implicitly convertible to other types depending on predefined conversion rules, inheritance structure, and explicit cast definitions.
Many predefined value types have predefined conversions to other predefined value types. If the type conversion is guaranteed not to lose information, the conversion can beimplicit (i.e. an explicitcast is not required).
A value can be implicitly converted to any class from which it inherits or interface that it implements. To convert a base class to a class that inherits from it, the conversion must be explicit in order for the conversion statement to compile. Similarly, to convert an interface instance to a class that implements it, the conversion must be explicit in order for the conversion statement to compile. In either case, the runtime environment throws a conversion exception if the value to convert is not an instance of the target type or any of its derived types.
The scope and extent of variables is based on their declaration. The scope of parameters and local variables corresponds to the declaring method or statement block, while the scope of fields is associated with the instance or class and is potentially further restricted by the field's access modifiers.
The extent of variables is determined by the runtime environment using implicit reference counting and a complex garbage collection algorithm.