Language elements (constants, numbers and strings)

Language elements (constants, numbers and strings)

Types

C# supports two kinds of types: value types and reference types. Value types include simple types (e.g., char, int, and float), enum types, and struct types. Reference types include class types, interface types, delegate types, and array types.

Value types differ from reference types in that variables of the value types directly contain their data, whereas variables of the reference types store references to 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.

The example

using System;
class Class1
{
   public int Value = 0;
}
class Test
{
   static void Main() {
      int val1 = 0;
      int val2 = val1;
      val2 = 123;
      Class1 ref1 = new Class1();
      Class1 ref2 = ref1;
      ref2.Value = 123;
      Console.WriteLine("Values: {0}, {1}", val1, val2);
      Console.WriteLine("Refs: {0}, {1}", ref1.Value, ref2.Value);
   }
}

shows this difference. The output produced is

Values: 0, 123
Refs: 123, 123

The assignment to the local variable val1 does not impact the local variable val2 because both local variables are of a value type (the type int) and each local variable of a value type has its own storage. In contrast, the assignment ref2.Value = 123; affects the object that both ref1 and ref2 reference.

The lines

Console.WriteLine("Values: {0}, {1}", val1, val2);
Console.WriteLine("Refs: {0}, {1}", ref1.Value, ref2.Value);

deserve further comment, as they demonstrate some of the string formatting behavior of Console.WriteLine, which, in fact, takes a variable number of arguments. The first argument is a string, which may contain numbered placeholders like {0} and {1}. Each placeholder refers to a trailing argument with {0} referring to the second argument, {1} referring to the third argument, and so on. Before the output is sent to the console, each placeholder is replaced with the formatted value of its corresponding argument.

Developers can define new value types through enum and struct declarations, and can define new reference types via class, interface, and delegate declarations. The example

using System;
public enum Color
{
   Red, Blue, Green
}
public struct Point 
{ 
   public int x, y; 
}
public interface IBase
{
   void F();
}
public interface IDerived: IBase
{
   void G();
}
public class A
{
   protected virtual void H() {
      Console.WriteLine("A.H");
   }
}
public class B: A, IDerived 
{
   public void F() {
      Console.WriteLine("B.F, implementation of IDerived.F");
   }
   public void G() {
      Console.WriteLine("B.G, implementation of IDerived.G");
   }
   override protected void H() {
      Console.WriteLine("B.H, override of A.H");
   }
}
public delegate void EmptyDelegate();

shows an example of each kind of type declaration.

Predefined types

C# provides a set of predefined types, most of which will be familiar to C and C++ developers.

The predefined reference types are object and string. The type object is the ultimate base type of all other types. The type string is used to represent Unicode string values. Values of type string are immutable.

The predefined value types include signed and unsigned integral types, floating point types, and the types bool, char, and decimal. The signed integral types are sbyte, short, int, and long; the unsigned integral types are byte, ushort, uint, and ulong; and the floating point types are float and double.

The bool type is used to represent Boolean values: values that are either true or false. The inclusion of bool makes it easier to write self-documenting code, and also helps eliminate the all-too-common C++ coding error in which a developer mistakenly uses “=” when “==” should have been used. In C#, the example

int i = ...;
F(i);
if (i = 0)   // Bug: the test should be (i == 0)
   G();

results in a compile-time error because the expression i = 0 is of type int, and if statements require an expression of type bool.

The char type is used to represent Unicode characters. A variable of type char represents a single 16-bit Unicode character.

The decimal type is appropriate for calculations in which rounding errors caused by floating point representations are unacceptable. Common examples include financial calculations such as tax computations and currency conversions. The decimal type provides 28 significant digits.

The table below lists the predefined types, and shows how to write literal values for each of them.

Type Description Example
object
The ultimate base type of all other types
object o = null;
string
String type; a string is a sequence of Unicode characters
string s = "hello";
sbyte
8-bit signed integral type
sbyte val = 12;
short
16-bit signed integral type
short val = 12;
int
32-bit signed integral type
int val = 12;
long
64-bit signed integral type
long val1 = 12;
long val2 = 34L;
byte
8-bit unsigned integral type
byte val1 = 12;
ushort
16-bit unsigned integral type
ushort val1 = 12;
uint
32-bit unsigned integral type
uint val1 = 12;
uint val2 = 34U;
ulong
64-bit unsigned integral type
ulong val1 = 12;
ulong val2 = 34U;
ulong val3 = 56L;
ulong val4 = 78UL;
float
Single-precision floating point type
float val = 1.23F;
double
Double-precision floating point type
double val1 = 1.23;
double val2 = 4.56D;
bool
Boolean type; a bool value is either true or false
bool val1 = true;
bool val2 = false;
char
Character type; a char value is a Unicode character
char val = 'h';
decimal
Precise decimal type with 28 significant digits
decimal val = 1.23M;

Each of the predefined types is shorthand for a system-provided type. For example, the keyword int refers to the struct System.Int32. As a matter of style, use of the keyword is favored over use of the complete system type name.

Predefined value types such as int are treated specially in a few ways but are for the most part treated exactly like other structs. Operator overloading enables developers to define new struct types that behave much like the predefined value types. For instance, a Digit struct can support the same mathematical operations as the predefined integral types, and can define conversions between Digit and predefined types.

The predefined types employ operator overloading themselves. For example, the comparison operators == and != have different semantics for different predefined types:

  • Two expressions of type int are considered equal if they represent the same integer value.
  • Two expressions of type object are considered equal if both refer to the same object, or if both are null.
  • Two expressions of type string are considered equal if the string instances have identical lengths and identical characters in each character position, or if both are null.

The example

using System;
class Test
{
   static void Main() {
      string s = "Test";
      string t = string.Copy(s);
      Console.WriteLine(s == t);
      Console.WriteLine((object)s == (object)t);
   }
}

produces the output

True
False

because the first comparison compares two expressions of type string, and the second comparison compares two expressions of type object.

Types, Variables, and Values
C# is a strongly-typed language. Every variable and constant has a type, as does every expression that evaluates to a value. Every method signature specifies a type for each input parameter and for the return value. The .NET Framework class library defines a set of built-in numeric types as well as more complex types that represent a wide variety of logical constructs, such as the file system, network connections, collections and arrays of objects, and dates. A typical C# program uses types from the class library as well as user-defined types that model the concepts that are specific to the program’s problem domain.

  • The information stored in a type can include the following:
  • The storage space that a variable of the type requires.
  • The maximum and minimum values that it can represent.
  • The members (methods, fields, events, and so on) that it contains.
  • The base type it inherits from.
  • The location where the memory for variables will be allocated at run time.
  • The kinds of operations that are permitted.

The compiler uses type information to make sure that all operations that are performed in your code are type safe. For example, if you declare a variable of type int, the compiler allows you to use the variable in addition and subtraction operations. If you try to perform those same operations on a variable of type bool, the compiler generates an error, as shown in the following example:
int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator ‘+’ cannot be applied to operands of type ‘int’ and ‘bool’.
int c = a + test;

Note – C and C++ developers, notice that in C#, bool is not convertible to int.

The compiler embeds the type information into the executable file as metadata. The common language runtime (CLR) uses that metadata at run time to further guarantee type safety when it allocates and reclaims memory.
Specifying Types in Variable Declarations

When you declare a variable or constant in a program, you must either specify its type or use the var keyword to let the compiler infer the type. The following example shows some variable declarations that use both built-in numeric types and complex user-defined types:

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = ‘C’;
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
where item <= limit
select item;

The types of method parameters and return values are specified in the method signature. The following signature shows a method that requires an int as an input argument and returns a string:

public string GetName(int ID)
{
if (ID < names.Length)
return names[ID];
else
return String.Empty;
}
private string[] names = { “Spencer”, “Sally”, “Doug” };

After a variable is declared, it cannot be re-declared with a new type, and it cannot be assigned a value that is not compatible with its declared type. For example, you cannot declare an int and then assign it a Boolean value of true. However, values can be converted to other types, for example when they are assigned to new variables or passed as method arguments. A type conversion that does not cause data loss is performed automatically by the compiler. A conversion that might cause data loss requires a cast in the source code.

Built-in Types

C# provides a standard set of built-in numeric types to represent integers, floating point values, Boolean expressions, text characters, decimal values, and other types of data. There are also built-in string and object types. These are available for you to use in any C# program.

You use the struct, class, interface, and enum constructs to create your own custom types. The .NET Framework class library itself is a collection of custom types provided by Microsoft that you can use in your own applications. By default, the most frequently used types in the class library are available in any C# program. Others become available only when you explicitly add a project reference to the assembly in which they are defined. After the compiler has a reference to the assembly, you can declare variables (and constants) of the types declared in that assembly in source code.

The Common Type System
It is important to understand two fundamental points about the type system in the .NET Framework:

It supports the principle of inheritance. Types can derive from other types, called base types. The derived type inherits (with some restrictions) the methods, properties, and other members of the base type. The base type can in turn derive from some other type, in which case the derived type inherits the members of both base types in its inheritance hierarchy. All types, including built-in numeric types such as System.Int32 (C# keyword: int), derive ultimately from a single base type, which is System.Object (C# keyword: object). This unified type hierarchy is called the Common Type System (CTS).

Each type in the CTS is defined as either a value type or a reference type. This includes all custom types in the .NET Framework class library and also your own user-defined types. Types that you define by using the struct keyword are value types; all the built-in numeric types are structs. Types that you define by using the class keyword are reference types. Reference types and value types have different compile-time rules, and different run-time behavior.

The following illustration shows the relationship between value types and reference types in the CTS.

Value Types and Reference Types

cts

Note – You can see that the most commonly used types are all organized in the System namespace. However, the namespace in which a type is contained has no relation to whether it is a value type or reference type.
Value Types

Value types derive from System.ValueType, which derives from System.Object. Types that derive from System.ValueType have special behavior in the CLR. Value type variables directly contain their values, which means that the memory is allocated inline in whatever context the variable is declared. There is no separate heap allocation or garbage collection overhead for value-type variables.

There are two categories of value types: struct and enum.

The built-in numeric types are structs, and they have properties and methods that you can access:
// Static method on type Byte.
byte b = Byte.MaxValue;

But you declare and assign values to them as if they were simple non-aggregate types:

byte num = 0xA;
int i = 5;
char c = ‘Z’;

Value types are sealed, which means, for example, that you cannot derive a type from System.Int32, and you cannot define a struct to inherit from any user-defined class or struct because a struct can only inherit from System.ValueType. However, a struct can implement one or more interfaces. You can cast a struct type to an interface type; this causes a boxing operation to wrap the struct inside a reference type object on the managed heap. Boxing operations occur when you pass a value type to a method that takes a System.Object as an input parameter.

You use the struct keyword to create your own custom value types. Typically, a struct is used as a container for a small set of related variables, as shown in the following example:

public struct CoOrds
{
public int x, y;

public CoOrds(int p1, int p2)
{
x = p1;
y = p2;
}
}

The other category of value types is enum. An enum defines a set of named integral constants. For example, the System.IO.FileMode enumeration in the .NET Framework class library contains a set of named constant integers that specify how a file should be opened. It is defined as shown in the following example:

public enum FileMode
{
CreateNew = 1,
Create = 2,
Open = 3,
OpenOrCreate = 4,
Truncate = 5,
Append = 6,
}

The System.IO.FileMode.Create constant has a value of 2. However, the name is much more meaningful for humans reading the source code, and for that reason it is better to use enumerations instead of constant literal numbers.

All enums inherit from System.Enum, which inherits from System.ValueType. All the rules that apply to structs also apply to enums.
Reference Types

A type that is defined as a class, delegate, array, or interface is a reference type. At run time, when you declare a variable of a reference type, the variable contains the value null until you explicitly create an instance of the object by using the new operator, or assign it an object that has been created elsewhere by using new, as shown in the following example:

MyClass mc = new MyClass();
MyClass mc2 = mc;

An interface must be initialized together with a class object that implements it. If MyClass implements IMyInterface, you create an instance of IMyInterface as shown in the following example:

IMyInterface iface = new MyClass();

When the object is created, the memory is allocated on the managed heap, and the variable holds only a reference to the location of the object. Types on the managed heap require overhead both when they are allocated and when they are reclaimed by the automatic memory management functionality of the CLR, which is known as garbage collection. However, garbage collection is also highly optimized, and in most scenarios it does not create a performance issue.

All arrays are reference types, even if their elements are value types. Arrays implicitly derive from the System.Array class, but you declare and use them with the simplified syntax that is provided by C#, as shown in the following example:

// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.
int len = nums.Length;

Reference types fully support inheritance. When you create a class, you can inherit from any other interface or class that is not defined as sealed, and other classes can inherit from your class and override your virtual methods.
Types of Literal Values

In C#, literal values receive a type from the compiler. You can specify how a numeric literal should be typed by appending a letter to the end of the number. For example, to specify that the value 4.56 should be treated as a float, append an “f” or “F” after the number: 4.56f. If no letter is appended, the compiler will infer a type for the literal.

Because literals are typed, and all types derive ultimately from System.Object, you can write and compile code such as the following:

string s = “The answer is ” + 5.ToString();
// Outputs: “The answer is 5”
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: “System.Int32”
Console.WriteLine(type);

Generic Types
A type can be declared with one or more type parameters that serve as a placeholder for the actual type (the concrete type) that client code will provide when it creates an instance of the type. Such types are called generic types. For example, the .NET Framework type System.Collections.Generic.List<T> has one type parameter that by convention is given the name T. When you create an instance of the type, you specify the type of the objects that the list will contain, for example, string:

List<string> strings = new List<string>();

The use of the type parameter makes it possible to reuse the same class to hold any type of element, without having to convert each element to object. Generic collection classes are called strongly-typed collections because the compiler knows the specific type of the collection’s elements and can raise an error at compile-time if, for example, you try to add an integer to the strings object in the previous example.
Implicit Types, Anonymous Types, and Nullable Types

As stated previously, you can implicitly type a local variable (but not class members) by using the var keyword. The variable still receives a type at compile time, but the type is provided by the compiler.

In some cases, it is inconvenient to create a named type for simple sets of related values that you do not intend to store or pass outside method boundaries. You can create anonymous types for this purpose.

Ordinary value types cannot have a value of null. However, you can create nullable value types by affixing a ? after the type. For example, int? is an int type that can also have the value null. In the CTS, nullable types are instances of the generic struct type System.Nullable<T>. Nullable types are especially useful when you are passing data to and from databases in which numeric values might be null.

Constants

Classes and structs can declare constants as members. Constants are values which are known at compile time and do not change. (To create a constant value that is initialized at runtime, use the readonly keyword.) Constants are declared as a field, using the const keyword before the type of the field. Constants must be initialized as they are declared. For example:

class Calendar1
{
    public const int months = 12;
}

In this example, the constant months will always be 12, and cannot be changed — even by the class itself. Constants must be of an integral type (sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, or string), an enumeration, or a reference to null.

Multiple constants of the same type can be declared at the same time, for example:

class Calendar2
{
    const int months = 12, weeks = 52, days = 365;
}

The expression used to initialize a constant can refer to another constant so long as it does not create a circular reference. For example:

class Calendar3
{
    const int months = 12;
    const int weeks = 52;
    const int days = 365;

    const double daysPerWeek = days / weeks;
    const double daysPerMonth = days / months;
}

Constants can be marked as public, private, protected, internal, or protected internal. These access modifiers define how users of the class can access the constant.

Constants are accessed as if they were static fields, although they cannot use the static keyword. Expressions that are not contained within the class defining the constant must use the class name, a period, and the name of the constant to access the constant.

Numbers

C# supplies several numeric data types for handling numbers in various representations. Integral types represent only whole numbers, and nonintegral types represent numbers with both integer and fractional parts.

Integer Data Types

Type Description Minimum Maximum Bits
bool Boolean flag false true 1
byte Unsigned Byte 0 255 8
sbyte Signed Byte -128 127 8
short Signed Short Integer -32,768 32,767 16
ushort Unsigned Short Integer 0 65,535 16
int Signed Integer -2,147,483,648 2,147,483,647 32
uint Unsigned Integer 0 4,294,967,295 32
long Signed Long Integer -9×1018 9×1018 64
ulong Unsigned Long Integer 0 1.8×1019 64

Non-Integer (Floating Point) Data Types

Type Description Scale Precision Bits
float Single Precision Number ±1.5×10-45 to ±3.4×1038 7 digits 32
double Double Precision Number ±5×10-324 to ±1.7×10308 15 or 16 digits 64
decimal Decimal Number ±10-28 to ±7.9×1028 28 or 29 digits 128

String

A C# string is an array of characters declared using the string keyword. A string literal is declared using quotation marks, as shown in the following example:
string s = “Hello, World!”;

You can extract substrings, and concatenate strings, like this:

string s1 = “orange”;
string s2 = “red”;

s1 += s2;
System.Console.WriteLine(s1);  // outputs “orangered”

s1 = s1.Substring(2, 5);
System.Console.WriteLine(s1);  // outputs “anger”

String objects are immutable, meaning that they cannot be changed once they have been created. Methods that act on strings actually return new string objects. In the previous example, when the contents of s1 and s2 are concatenated to form a single string, the two strings containing “orange” and “red” are both unmodified. The += operator creates a new string that contains the combined contents. The result is that s1 now refers to a different string altogether. A string containing just “orange” still exists, but is no longer referenced when s1 is concatenated.

Note – Use caution when creating references to strings. If you create a reference to a string, and then “modify” the string, the reference will continue to point to the original object, not the new object that was created when the string was modified. The following code illustrates the danger:

string s1 = “Hello”;
string s2 = s1;
s1 += ” and goodbye.”;
Console.WriteLine(s2); //outputs “Hello”

Because modifications to strings involve the creation of new string objects, for performance reasons, large amounts of concatenation or other involved string manipulation should be performed with the StringBuilder class, like this:

System.Text.StringBuilder sb = new System.Text.StringBuilder();
sb.Append(“one “);
sb.Append(“two “);
sb.Append(“three”);
string str = sb.ToString();

Language Basics
Variables types, constants and objects

Get industry recognized certification – Contact us

keyboard_arrow_up
Open chat
Need help?
Hello 👋
Can we help you?