C# Programming/Variables
Variables are used to store values. More technically, a variable binds an object (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:
string name = "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 explicit type. 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 type string
. This is a legal statement. However, if we had said int name
= "Dr. Jones"
, the compiler would have thrown an error telling us that you cannot implicitly convert between int
and string
. There are methods for doing this, but we will talk about them later.
Fields, local variables, and parameters
[edit | edit source]C# supports several program elements corresponding to the general programming concept of variable: fields, parameters, and local variables.
Fields
[edit | edit source]Fields, sometimes called class-level variables, are variables associated with classes or structures. An instance variable is a field associated with an instance of the class or structure, while a static variable, declared with the static keyword, is a field associated with the type itself. Fields can also be associated with their class by making them constants (const), which requires a declaration assignment of a constant value and prevents subsequent changes to the field.
Each field has a visibility of public, protected, internal, protected internal, or private (from most visible to least visible).
Local variables
[edit | edit source]Like fields, local variables can optionally be constant (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.
Parameter
[edit | edit source]Parameters are variables associated with a method.
An in 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++.
An out 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 be unbound upon method entry, thus it is illegal to reference an out parameter before assigning it a value. It also must be assigned by the method in each valid (non-exceptional) code path through the method in order for the method to compile.
A reference parameter is similar to an out parameter, except that it is bound before the method call and it need not be assigned by the method.
A params parameter represents a variable number of parameters. If a method signature includes one, the params 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:
void MethodOne(int param1) // definition
MethodOne(variable); // call
// Out param:
void MethodTwo(out string message) // definition
MethodTwo(out variable); // call
// Reference param;
void MethodThree(ref int someFlag) // definition
MethodThree(ref theFlag) // call
// Params
void MethodFour(params string[] names) // definition
MethodFour("Matthew", "Mark", "Luke", "John"); // call
Types
[edit | edit source]Each type in C# is either a value type or a reference 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.
Value types
[edit | edit source]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.
- Value types are always copied (intrinsically) before being passed to a method. Changes to this new object will not be reflected back in the original object passed into the method.
- Value types do not /need/ you to call their constructor. They are automatically initialized.
- Value types always initialize their fields to 0 or null.
- Value types can NEVER be assigned a value of null (but can using Nullable types)
- Value types sometimes need to be boxed (wrapped inside an object), allowing their values to be used like objects.
Reference types
[edit | edit source]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:
- The CLR calculates the amount of memory required to hold the object on the heap.
- The CLR inserts the data into the newly created memory space.
- The CLR marks where the end of the space lies, so that the next object can be placed there.
- The CLR returns a reference to the newly created space.
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.
Integral types
[edit | edit source]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#
public void UsingCSharpTypeAlias()
{
int i = 42;
}
public void EquivalentCodeWithoutAlias()
{
System.Int32 i = 42;
}
' Visual Basic .NET
Public Sub UsingVisualBasicTypeAlias()
Dim i As Integer = 42
End Sub
Public Sub EquivalentCodeWithoutAlias()
Dim i As System.Int32 = 42
End Sub
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 each value type a consistent size across platforms and compilers. That consistency is an important distinction from other languages such as C, where, e.g. a long
is only guaranteed to be at least as large as an int
, and is implemented with different sizes by different compilers. As reference types, variables of types derived from object
(i.e. any class
) are exempt from the consistent size requirement. That is, the size of reference types like System.IntPtr
, as opposed to value types like System.Int32
, may vary by platform. Fortunately, there is rarely a need to know the actual size of a reference type.
There are two predefined reference types: object
, an alias for the System.Object
class, from which all other reference types derive; and string
, an alias for the System.String
class. C# likewise has several integral value types, each an alias to a corresponding value type in the System
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's System.Int32
type implements a ToString()
method to convert the value of an integer to its string representation, C#'s int
type exposes that method:
int i = 97;
string s = i.ToString(); // The value of s is now the string "97".
Likewise, the System.Int32
type implements the Parse()
method, which can therefore be accessed via C#'s int
type:
string s = "97";
int i = 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 as casting.
object boxedInteger = 97;
int unboxedInteger = (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.
object getInteger = "97";
int anInteger = (int) getInteger; // No compile-time error. The program will crash, however.
The built-in C# type aliases and their equivalent .NET Framework types follow:
Integers
[edit | edit source]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 |
Floating-point
[edit | edit source]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 |
Other predefined types
[edit | edit source]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. |
Custom types
[edit | edit source]The predefined types can be aggregated and extended into custom types.
Custom value types are declared with the struct or enum keyword. Likewise, custom reference types are declared with the class keyword.
Arrays
[edit | edit source]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 = new string[5];
As with other variable types, the declaration and the initialization can be combined:
string[] a_str = new string[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:
static void swap (int[] a_iArray, int iI, int iJ)
{
int iTemp = 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 = new ushort[234];
[...]
for (ushort us = 0; us < a_usNumbers.Length; us++)
{
a_usNumbers[us] = us;
}
Since C# 2.0, it is possible to have arrays also inside of structures.
Text & variable example
[edit | edit source]using System;
namespace Login
{
class Username_Password
{
public static void Main()
{
string username,password;
Console.Write("Enter username: ");
username = Console.ReadLine();
Console.Write("Enter password: ");
password = Console.ReadLine();
if (username == "SomePerson" && password == "SomePassword")
{
Console.WriteLine("Access Granted.");
}
else if (username != "SomePerson" && password == "SomePassword")
{
Console.WriteLine("The username is wrong.");
}
else if (username == "SomePerson" && password != "SomePassword")
{
Console.WriteLine("The password is wrong.");
}
else
{
Console.WriteLine("Access Denied.");
}
}
}
}
Conversion
[edit | edit source]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.
Predefined conversions
[edit | edit source]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 be implicit (i.e. an explicit cast is not required).
Inheritance polymorphism
[edit | edit source]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.
Scope and extent
[edit | edit source]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.