C# Lectures - Lecture 3 Designing Types in C#. Basics You Need to Know About Classes






4.94/5 (35 votes)
Third lecture from the series I'm running. Related to class definition and gives basic understanding about what is type in .NET
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:
//value types
internal enum eMyNumbers
{
ONE = 1,
TWO,
THREE
}
internal struct ExampleStructure
{
//public ExampleStructure(); - this line will not compile
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; }
}
}
//reference types
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;
//explicit default constructor is called
ExampleStructure structValue = new ExampleStructure();//stored in stack
structValue.IntValue = 5;//changes in stack
structValue.StringValue = " sample string";//changes in stack
//copying on assignment operator below
//creates new structure in stack and copies there all values
ExampleStructure structValue2 = structValue;
Console.WriteLine(structValue.IntValue + structValue.StringValue); //prints "5 sample string"
Console.WriteLine(structValue2.IntValue + structValue2.StringValue); //prints "5 sample string"
Console.WriteLine(Object.ReferenceEquals(structValue, structValue2));//prints false
ExampleClass classSample = new ExampleClass();//stored in heap
classSample.IntValue = 5;//changes in heap
classSample.StringValue = " sample string";
ExampleClass classSample2 = classSample;//copies only reference
Console.WriteLine(classSample.IntValue + classSample.StringValue); //prints "5 sample string"
Console.WriteLine(classSample2.IntValue + classSample2.StringValue); //prints "5 sample string"
Console.WriteLine(Object.ReferenceEquals(classSample, classSample2));//prints true
object o = (object)structValue;//boxing
structValue2 = (ExampleStructure)o;//unboxing
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 tonull
. - 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, notstatic
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 typeinternal
- Is visible only inside its own assembly. If there is no value for the access modifier by default type isinternal
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 typesprotected
- Can be accessed by type members, nested types and derived typesinternal
- Can be accessed by methods of current assemblyprotected internal
- Can be accessed by methods of nested types, derived types and methods of current assemblypublic
- 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 keyword means that class if visible only inside current assembly
internal class DemonstratingType
{
//Constant member that is equal to all objects
//of the class and is not changeable
public const int const_digit = 5;
//static field that is equal to all objects
//and related to type not to object
private static string m_StaticString;
//read only field
//it can be changed only in constructor
public readonly int ReadOnlyDigit;
//static property that wraps static string
public static string StaticString
{
get { return DemonstratingType.m_StaticString; }
set { DemonstratingType.m_StaticString = value; }
}
//not static filed that is unique for each
//object and related to object not to type
private string m_InstanceString;
//instance property that wraps instance field
public string InstanceString
{
get { return m_InstanceString; }
set { m_InstanceString = value; }
}
protected string m_ProtectedInstanceString;
//+ operator overloading
public static string operator+ (DemonstratingType obj1, DemonstratingType obj2)
{
return obj1.m_InstanceString + obj2.m_InstanceString;
}
//type constructor
static DemonstratingType()
{
m_StaticString = "static string default value";
}
//default constructor
public DemonstratingType()
{
m_ProtectedInstanceString = "default value for protected string";
ReadOnlyDigit = 10;
}
//parametrized overloaded constructor
public DemonstratingType(string InstanceStringInitialValue)
{
m_InstanceString = InstanceStringInitialValue;
m_ProtectedInstanceString = "default value for protected string";
ReadOnlyDigit = 10;
}
//static method that is called on type
public static int SummarizeTwoDigits(int a, int b)
{
return a + b;
}
//instance method that is called on object
public int MyDigitPlustTypeConstant(int digit)
{
return digit + const_digit;
}
public string ShowProtectedString()
{
return m_ProtectedInstanceString;
}
//nested type
private class InternalDataClass
{
private int m_x;
private int m_y;
public InternalDataClass(int x, int y)
{
m_x = x;
m_y = y;
}
}
}
//class DerivedDemonstratedType derives DemonstratingType
//this is called inheritance
internal sealed class DerivedDemonstratingType : DemonstratingType
{
//this function changes protected string that we
//derived from parent type in our sample only
//derived class may change protected string
public void ChangeProtectedString(string newString)
{
m_ProtectedInstanceString = newString;
}
}
Usage:
Console.WriteLine("-----------------DESIGNING TYPES-------------------");
//default constructor
DemonstratingType object1 = new DemonstratingType();
DemonstratingType object2 = new DemonstratingType();
//static field and static property
Console.WriteLine(DemonstratingType.StaticString);//prints "static string default value"
DemonstratingType.StaticString = "this is the static string";
Console.WriteLine(DemonstratingType.const_digit);//prints 5
Console.WriteLine(DemonstratingType.StaticString);//prints "this is the static string"
//instance field and instance property
object1.InstanceString = "object 1 string";
object2.InstanceString = " object 2 string";
Console.WriteLine(object1.InstanceString);//prints "object 1 string"
Console.WriteLine(object2.InstanceString);//prints " object 2 string"
//operator overloading
Console.WriteLine(object1 + object2);//prints "object 1 string object 2 string"
//parametrized overloaded constructor
DemonstratingType object3 = new DemonstratingType("object 3 string");
Console.WriteLine(object3.InstanceString);//prints "object 3 string"
//static method
Console.WriteLine(DemonstratingType.SummarizeTwoDigits(2 , 3));//prints 5
//instance method
Console.WriteLine(object3.MyDigitPlustTypeConstant(5));//prints 10
//inheritance example + protected string example
DerivedDemonstratingType childType = new DerivedDemonstratingType();
//object1 will reference to same object that childType
object1 = childType;
Console.WriteLine(object1.ShowProtectedString());//prints "default value for protected string"
childType.ChangeProtectedString("new value for protected string");
Console.WriteLine(object1.ShowProtectedString());//prints "new value for protected string"
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 returnstring
representative of their value in this method.Equals
- Returnstrue
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 typeType
that identifies object that calledGetType
. Object of type Type can be used to get information about metadata of object that calledGetType
. This is implemented using classes fromSystem.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 thatGetType
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 whenGarbageCollector
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
{
//static variable is used in our example
//as global counter for all objects of
//type 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()
{
//here in addition to the full name of the type
//that System.Object returns we add the value
//of string member
return base.ToString() + m_InternalString;
}
public override int GetHashCode()
{
//instead of HashCode that is implemented
//in System.Object we return their order
//number that we give to each object while
//its creation
return m_OrderNumber;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
//check if input object is of type ObjectOverrideExample
//if so then we compare not that both references point
//to same object but we compare order numbers
if (obj.GetType().FullName == "_03_ClassesStructuresEtc.Program+ObjectOverrideExample")
{
ObjectOverrideExample temp = (ObjectOverrideExample)obj;
if (m_OrderNumber != temp.OrderNumber)
{
return false;
}
return true;
}
//if input object is of different type we compare
//that both objects reference the same memory i.e. use
//parent algorithm
return base.Equals(obj);
}
}
Usage:
Console.WriteLine("-----------------System.Object-------------------");
ObjectOverrideExample OverrideSample = new ObjectOverrideExample();
Console.WriteLine(OverrideSample.ToString());//prints "_03_ClassesStructuresEtc.Program+
//ObjectOverrideExample Private string"
ObjectExample Sample = new ObjectExample();
Console.WriteLine(Sample.ToString());//prints "_03_ClassesStructuresEtc.Program+ObjectExample"
//GetHashCode
Console.WriteLine(Sample.GetHashCode());
Console.WriteLine(OverrideSample.GetHashCode());//prints 1
for (int i = 0; i < 10; i++) //prints numbers from 2 to 11
{
ObjectOverrideExample tmp = new ObjectOverrideExample();
Console.WriteLine(tmp.GetHashCode());
}
//Equals
ObjectOverrideExample tmp2 = new ObjectOverrideExample();
Console.WriteLine(OverrideSample.Equals(tmp2));//prints true
ObjectOverrideExample tmp3 = OverrideSample;
Console.WriteLine(OverrideSample.Equals(tmp3));//prints true
//GetType
Type OverrideType = OverrideSample.GetType();
Console.WriteLine(OverrideType.FullName);//prints "_03_ClassesStructuresEtc.Program+
// ObjectOverrideExample"
Console.WriteLine(OverrideType.Name); //prints "ObjectOverrideExample"
Type SampleType = Sample.GetType();
Console.WriteLine(SampleType.FullName);//prints
// "_03_ClassesStructuresEtc.Program+ObjectExample"
Console.WriteLine(SampleType.Name); //prints "ObjectExample"
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
{
//ChildClass_Level1 overrides base class function
//to have its own implementation
public override void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ChildClass_Level1" + s);
}
}
internal class ChildClass_Level2 : ChildClass_Level1
{
//ChildClass_Level2 overrides base class function
//to have its own implementation
public override void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ChildClass_Level2" + s);
}
}
Usage:
Console.WriteLine("-----------------TYPE CASTING-------------------");
//implicit conversion from child to parent, no special syntax is needed
ParentClass parent = new ParentClass();
parent.OutputFunction();//prints "OutputFunction in ParentClass"
ParentClass parent_child1 = new ChildClass_Level1();
parent_child1.OutputFunction();//prints "OutputFunction in ChildClass_Level1"
ParentClass parent_child2 = new ChildClass_Level2();
parent_child2.OutputFunction();//prints "OutputFunction in ChildClass_Level2"
//explicit conversion to cast back to derived type
ChildClass_Level2 child2 = (ChildClass_Level2)parent_child2;
child2.OutputFunction();//prints "OutputFunction in ChildClass_Level2"
ChildClass_Level1 child1 = (ChildClass_Level1)parent_child1;
child1.OutputFunction();//prints "OutputFunction in ChildClass_Level1"
//the code below compiles, but fails in runtime
try
{
child2 = (ChildClass_Level2)parent;
child2.OutputFunction();
}
catch (InvalidCastException e)
{
Console.WriteLine("Catch invalid cast exception : " + e.Message);
}
//to avoid exception above we can use following code and is operator
if (parent is ChildClass_Level2)
{
parent.OutputFunction();//we never reach here
}
if(parent_child2 is ChildClass_Level2)
{
parent_child2.OutputFunction
(" using is");//prints "OutputFunction in ChildClass_Level2 using is"
}
//is operator can be replaced by as operator
child2 = parent as ChildClass_Level2;
if (child2 != null)
{
child2.OutputFunction();//we never reach here
}
child2 = parent_child2 as ChildClass_Level2;
if (child2 != null)
{
child2.OutputFunction
(" using as");//prints "OutputFunction in ChildClass_Level2 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/