Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
4 Oct 2016 2  
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


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 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 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 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
{
    //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

  1. Jeffrey Richter - CLR via C# 4.5
  2. Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
  3. http://www.introprogramming.info/english-intro-csharp-book/read-online/chapter-14-defining-classes/

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here