Full Lectures Set
- C#Lectures - Lecture 1: Primitive Types
- C# Lectures - Lecture 2: Work with text in C#: char, string, StringBuilder, SecureString
- C# Lectures - Lecture 3 Designing Types in C#. Basics You Need to Know About Classes
- C# Lectures - Lecture 4: OOP basics: Abstraction, Encapsulation, Inheritance, Polymorphism by C# example
- C# Lectures - Lecture 5:Events, Delegates, Delegates Chain by C# example
- C# Lectures - Lecture 6: Attributes, Custom attributes in C#
- C# Lectures - Lecture 7: Reflection by C# example
- C# Lectures - Lecture 8: Disaster recovery. Exceptions and error handling by C# example
- C# Lectures - Lecture 9:Lambda expressions
- C# Lectures - Lecture 10: LINQ introduction, LINQ to 0bjects Part 1
- C# Lectures - Lecture 11: LINQ to 0bjects Part 2. Nondeferred Operators
Introduction
In this article, I will focus on how to define type in .NET using C#. I'll review the difference of reference types and values types, discuss about System.Object
and type casting. Someone may want to see the explanation of encapsulation, inheritance and ploymorphism here. I decided to have a separate article for it as the next one in this series, so it is coming soon...
Value Types and Reference Types
CLR supports two kinds of types: reference types and value types. Value types are primitive types (you can learn more about primitive types in my article here), enums and structures. Items of value types are stored in stack and value of the variable is stored in variable itself. This is relevant when you declare variable of value type alone, when it is part of the reference class it is located in heap. All value types are derived from System.ValueType
. Value type can't contain null
value. Interesting case of value types is structure. Structure in C# is very similar to class:
- It can hold variables
- Can have properties and methods
- Can implement interfaces
It also has differences from classes:
- Variables can't be assigned while declaration unless they are const or static:
- Structures can't inherit structures
- Structures are copied on assignment (all fields from new variable are copied to source) and after assignments current variable and source reference to different structures
- Structure can't be instantiated without using new operator
In .NET, there is an option to convert value types to reference types. We need to do it mostly to pass value type to some function that operates with reference types. If we need to work with value type through reference, we do a mechanism called boxing. Boxing is converting value type to reference type, when it happens the following set of actions is done:
- Memory required for value type fields + additional necessary fields is allocated
- Values of reference types are copying from stack to heap
- Address of newly created reference type is returned
Once type was boxed, there is an option to do opposite operation and do unboxing.
The following code demonstrates things described in this section:
Declaration:
internal enum eMyNumbers
{
ONE = 1,
TWO,
THREE
}
internal struct ExampleStructure
{
private int m_intValue;
private string m_stringValue;
public int IntValue
{
get { return m_intValue; }
set { m_intValue = value; }
}
public string StringValue
{
get { return m_stringValue; }
set { m_stringValue = value; }
}
}
internal class ExampleClass
{
private int m_intValue = 0;
private string m_StringValue = "default class value";
public int IntValue
{
get { return m_intValue; }
set { m_intValue = value; }
}
public string StringValue
{
get { return m_StringValue; }
set { m_StringValue = value; }
}
}
Usage:
Console.WriteLine("-----------------REFERENCE TYPES AND VALUE TYPES-------------------");
eMyNumbers enumSample = eMyNumbers.THREE;
ExampleStructure structValue = new ExampleStructure();structValue.IntValue = 5;structValue.StringValue = " sample string";ExampleStructure structValue2 = structValue;
Console.WriteLine(structValue.IntValue + structValue.StringValue); Console.WriteLine(structValue2.IntValue + structValue2.StringValue); Console.WriteLine(Object.ReferenceEquals(structValue, structValue2));
ExampleClass classSample = new ExampleClass();classSample.IntValue = 5;classSample.StringValue = " sample string";
ExampleClass classSample2 = classSample;Console.WriteLine(classSample.IntValue + classSample.StringValue); Console.WriteLine(classSample2.IntValue + classSample2.StringValue); Console.WriteLine(Object.ReferenceEquals(classSample, classSample2));
object o = (object)structValue;structValue2 = (ExampleStructure)o;
In contrast, reference types that are classes are stored in heap and variable of reference type holds reference to heap memory where object is located. Rest of this article is dedicated to reference types and class designing.
Designing Types
Object Oriented Programming is based on types that programmer uses from standard libraries such as .NET and on types that he defines by himself. Today's software development is built on classes (types) that developers create and use to solve their tasks and problems. C# has very well developed instruments to build your own types. In C#, type may have the following members:
- Constant - Constant member relates to type itself and not to object. Logically constants are
static
members. Constants are not accessible through objects names but through type name. Constant member can be only of built in type. Compiler writes constants to class metadata during compilation and user defined type that is initialized during runtime can't be a constant. Actually, there is one case when user defined type can be constant member, it is when it is assigned to null
.
- Field - Variable of some type available for reading or reading\writing. Field can be
static
and in this case, it is related to type and is the same for all objects or not static than it is unique for each object. Static
objects are not accessible through objects names but through type name, not static
members are accessible using objects.
- Constructor - Method that is specified for object fields initialization. Constructor can be overloaded as any other type method. The main requirements are to be
public
and have the same name as type name.
- Method - Function that type implements. This is the function related to manipulation with type data, object data or anything else. Method can be
static
or instance. Static method is called on type, instance method is related on object.
- Overloaded operator - Defines the action that should be performed on object when operator is applied on it.
- Cast operator - Methods that define how one object is casted to another.
- Property - This is the wrapper over the field that gives convenient mechanism to set and get its values controlled by developer mode.
- Event - Ability to send notification to static or instance method.
- Type - Type nested to current type.
There are three values for type visibility:
public
- Type is visible for every code that uses the assembly with type
internal
- Is visible only inside its own assembly. If there is no value for the access modifier by default type is internal
private
- Default value for nested types, but nested type can have all the rest modifiers as well
Besides access modifiers, you can apply partial keyword for class definition. This means that class implementation is realized in several files.
There are following values for members visibility:
private
- Can be accessed only by type members and nested types
protected
- Can be accessed by type members, nested types and derived types
internal
- Can be accessed by methods of current assembly
protected internal
- Can be accessed by methods of nested types, derived types and methods of current assembly
public
- Available to any method of any assembly
Besides visibility keywords, you can apply readonly
keyword to member variable. Readonly
variable can be assigned only in constructor. Readonly
can be applied to instance and static members.
Methods are members of the time that implement specific functionality. Methods can be static
and instance. One of the types of methods are constructors. Constructor:
- Has the same name as type
- Can't be inherited means that keywords virtual, new, override, sealed and abstract can't be applicable to constructor
- Constructor can be static for types and instance for objects
- Static constructor is parameterless and doesn't have visibility modifiers and they go private by default
- Static constructor may change only static fields
- You can define several constructors that have different signatures
- For structures, you can't define parameterless default constructor. It should be always parametrized.
Even if you don't define constructor CLR always generates default constructor that has the same name as type and doesn't receive any parameters.
The code below demonstrates things that I described in this section:
Declaration:
internal class DemonstratingType
{
public const int const_digit = 5;
private static string m_StaticString;
public readonly int ReadOnlyDigit;
public static string StaticString
{
get { return DemonstratingType.m_StaticString; }
set { DemonstratingType.m_StaticString = value; }
}
private string m_InstanceString;
public string InstanceString
{
get { return m_InstanceString; }
set { m_InstanceString = value; }
}
protected string m_ProtectedInstanceString;
public static string operator+ (DemonstratingType obj1, DemonstratingType obj2)
{
return obj1.m_InstanceString + obj2.m_InstanceString;
}
static DemonstratingType()
{
m_StaticString = "static string default value";
}
public DemonstratingType()
{
m_ProtectedInstanceString = "default value for protected string";
ReadOnlyDigit = 10;
}
public DemonstratingType(string InstanceStringInitialValue)
{
m_InstanceString = InstanceStringInitialValue;
m_ProtectedInstanceString = "default value for protected string";
ReadOnlyDigit = 10;
}
public static int SummarizeTwoDigits(int a, int b)
{
return a + b;
}
public int MyDigitPlustTypeConstant(int digit)
{
return digit + const_digit;
}
public string ShowProtectedString()
{
return m_ProtectedInstanceString;
}
private class InternalDataClass
{
private int m_x;
private int m_y;
public InternalDataClass(int x, int y)
{
m_x = x;
m_y = y;
}
}
}
internal sealed class DerivedDemonstratingType : DemonstratingType
{
public void ChangeProtectedString(string newString)
{
m_ProtectedInstanceString = newString;
}
}
Usage:
Console.WriteLine("-----------------DESIGNING TYPES-------------------");
DemonstratingType object1 = new DemonstratingType();
DemonstratingType object2 = new DemonstratingType();
Console.WriteLine(DemonstratingType.StaticString);DemonstratingType.StaticString = "this is the static string";
Console.WriteLine(DemonstratingType.const_digit);Console.WriteLine(DemonstratingType.StaticString);object1.InstanceString = "object 1 string";
object2.InstanceString = " object 2 string";
Console.WriteLine(object1.InstanceString);Console.WriteLine(object2.InstanceString);Console.WriteLine(object1 + object2);DemonstratingType object3 = new DemonstratingType("object 3 string");
Console.WriteLine(object3.InstanceString);Console.WriteLine(DemonstratingType.SummarizeTwoDigits(2 , 3));Console.WriteLine(object3.MyDigitPlustTypeConstant(5));DerivedDemonstratingType childType = new DerivedDemonstratingType();
object1 = childType;
Console.WriteLine(object1.ShowProtectedString());childType.ChangeProtectedString("new value for protected string");
Console.WriteLine(object1.ShowProtectedString());
Object Creation Flow
To create any object, you must call new
operator. CLR requires new
to be called to create any object. (There is a simplified object creation way for built in type without calling new
operator, but this is rather exception than a rule). When you call new, the following order of events happen:
- CLR calculates size in bytes that is required to save in memory all object fields, all parent types fields and two additional fields: type object pointer and sync block index.
- Memory calculated in the previous step is allocated and initialized by 0. Type object pointer and sync block index are initialized.
- Constructor of object is called + constructor of base class. In other words, all constructors till
System.Objects
will be called.
In contrast to C++, for example, operator new
in C# doesn't have paired delete
operator. CLR automatically cleans memory and you don't need to take care about it.
When object is created, all members are initialized by default values which are zeros for primitive types and null
for types. The table below demonstrates default values for types:
Type of the Field | Default Value |
bool | false |
byte | 0 |
char | '\0' |
string | null |
decimal | 0.0M |
double | 0.0D |
float | 0.0F |
int | 0 |
object reference | null |
System.Object - Parent for Everything in .NET
As you probably know, all types in .NET directly or indirectly are derived from System.Object
. Even primitive types in .NET are derived from ValueType
that in its turn are derived from System.Object
(you can learn more about primitive types in my article here). In this article, we will focus on non-primitive types that are classes. Basing on previous statements declarations:
class A
{
…..
}
and
class A: System.Object
{
…..
}
are equal. The fact that all classes are derived from System.Object
guarantees that every object or any type has minimal number of methods that it derives from System.Object
. Below, I describe public
and protected
methods that System.Object
implements and provides to each .NET type:
Public
methods:
ToString
- By default returns the full name of the type. Usually, developers override it with more meaningful functionality inside. For example, all primitive types return string
representative of their value in this method.
Equals
- Returns true
if two objects have the same values. Can be override and you can implement your own way of comparison for two objects.
GetType
- Returns object of type Type
that identifies object that called GetType
. Object of type Type can be used to get information about metadata of object that called GetType
. This is implemented using classes from System.Reflection
namespace. Reflection is a separate topic and we will not focus on it here. GetType
is not a virtual method and you can't override it. Based on it, you can be sure that GetType
always returns valid data that describe current object properly.
GetHashCode
- Returns hash code for current object. Can be overridden if you require it.
If you override Equals
, it is recommended to override GetHashCode
as well. Some .NET algorithms that manipulate with objects require that two equal objects should have same hash codes.
Protected
methods:
MemberwiseClone
- This method creates new item of type, copies all fields from object on which was called. Returns reference to new item.
Finalize
- is called when GarbageCollector
identifies that object is garbage but before freeing memory that object holds.
To demonstrate basic functionality that each class receives from System.Object
, I implemented a short example with 2 classes. Class ObjectExample
is an empty class that receives all System.Object
functionality by default and class ObjectOverrideExample
overrides Equals
, ToString
and GetHashCode
. Code that demonstrates it is provided below:
Declaration:
internal class ObjectExample
{
}
internal class ObjectOverrideExample
{
private static int ObjectsCounter = 0;
private string m_InternalString;
private int m_OrderNumber;
public int OrderNumber
{
get { return m_OrderNumber; }
set { m_OrderNumber = value; }
}
public string InternalString
{
get { return m_InternalString; }
set { m_InternalString = value; }
}
public ObjectOverrideExample()
{
m_InternalString = " Private string";
ObjectsCounter++;
m_OrderNumber = ObjectsCounter;
}
public override string ToString()
{
return base.ToString() + m_InternalString;
}
public override int GetHashCode()
{
return m_OrderNumber;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
if (obj.GetType().FullName == "_03_ClassesStructuresEtc.Program+ObjectOverrideExample")
{
ObjectOverrideExample temp = (ObjectOverrideExample)obj;
if (m_OrderNumber != temp.OrderNumber)
{
return false;
}
return true;
}
return base.Equals(obj);
}
}
Usage:
Console.WriteLine("-----------------System.Object-------------------");
ObjectOverrideExample OverrideSample = new ObjectOverrideExample();
Console.WriteLine(OverrideSample.ToString()); ObjectExample Sample = new ObjectExample();
Console.WriteLine(Sample.ToString());Console.WriteLine(Sample.GetHashCode());
Console.WriteLine(OverrideSample.GetHashCode());for (int i = 0; i < 10; i++) {
ObjectOverrideExample tmp = new ObjectOverrideExample();
Console.WriteLine(tmp.GetHashCode());
}
ObjectOverrideExample tmp2 = new ObjectOverrideExample();
Console.WriteLine(OverrideSample.Equals(tmp2));ObjectOverrideExample tmp3 = OverrideSample;
Console.WriteLine(OverrideSample.Equals(tmp3));Type OverrideType = OverrideSample.GetType();
Console.WriteLine(OverrideType.FullName); Console.WriteLine(OverrideType.Name); Type SampleType = Sample.GetType();
Console.WriteLine(SampleType.FullName); Console.WriteLine(SampleType.Name);
Types Casting
During runtime, CLR always knows about type of the current object. As we discussed, each object has GetType
function that returns its type. Knowing the type of each object at any given moment of time guarantees that application will run properly and, for example, proper object will be passed to function and proper method will be called on it. But quite often, we need to cast some types to another types to have, for example, same handling of different objects. CLR supports smooth type casting to parent types. You don't need to write any specific code for that. This is called implicit casting. Implicit casting is when you cast from derived type to base. If after you did implicit casting you need to cast back, you need to use explicit casting. Explicit casting is when you cast back from parent to child class.
Besides implicit and explicit casting, C# has two operators that are useful for types casting. is
operator checks if input object is compatible with current type and return true in this case. Operator is
never generates exception. Besides is
operator C# has another operator that is called as
. Using as operator you can check if object compatible with type and if so, as
returns not null pointer for this object, otherwise it returns null. Operator as is similar to explicit type casting operator, but it doesn't generate exception if object doesn't feet type. Operators is
and as
are very similar, but as
works faster.
The code below demonstrates types casting and is
, as
operators:
Declaration:
internal class ParentClass
{
public virtual void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ParentClass" + s);
}
}
internal class ChildClass_Level1 : ParentClass
{
public override void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ChildClass_Level1" + s);
}
}
internal class ChildClass_Level2 : ChildClass_Level1
{
public override void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ChildClass_Level2" + s);
}
}
Usage:
Console.WriteLine("-----------------TYPE CASTING-------------------");
ParentClass parent = new ParentClass();
parent.OutputFunction();ParentClass parent_child1 = new ChildClass_Level1();
parent_child1.OutputFunction();ParentClass parent_child2 = new ChildClass_Level2();
parent_child2.OutputFunction();ChildClass_Level2 child2 = (ChildClass_Level2)parent_child2;
child2.OutputFunction();ChildClass_Level1 child1 = (ChildClass_Level1)parent_child1;
child1.OutputFunction();try
{
child2 = (ChildClass_Level2)parent;
child2.OutputFunction();
}
catch (InvalidCastException e)
{
Console.WriteLine("Catch invalid cast exception : " + e.Message);
}
if (parent is ChildClass_Level2)
{
parent.OutputFunction();}
if(parent_child2 is ChildClass_Level2)
{
parent_child2.OutputFunction
(" using is");}
child2 = parent as ChildClass_Level2;
if (child2 != null)
{
child2.OutputFunction();}
child2 = parent_child2 as ChildClass_Level2;
if (child2 != null)
{
child2.OutputFunction
(" using as");}
Sources
- Jeffrey Richter - CLR via C# 4.5
- Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
- http://www.introprogramming.info/english-intro-csharp-book/read-online/chapter-14-defining-classes/