C# Encapsulation Syntax

Encapsulation of a Circle Example

Encapsulation is one of the main OOP concepts. It is summarised here, using a Circle class, which is kept simple from an algorithmic perspective, so that the logic does not ob­scure the syntax. We use it to illustrate many general object-oriented programming fea­tur­es and syntax, as found in most OO languages. We also illustrate some .NET & C# specialities, like properties and in­dex­ers (special kind of properties), which are relatively rare in other OO languages.

PREREQUISITES — You should already…
  • have learned the fundamentals of C# syntax;
  • understand static methods and static classes;
  • understand static function definitions, returns and overloading;
  • understand default parameters, named arguments and pass-by-reference;
  • understand references, scope, namespaces;
  • understand expressions, operators, precedence and operator association.

Encapsulation Concepts

The most fundamental concept in object-orientated programming (OOP), is encapsulation. This al­lows one to wrap” the func­tio­na­li­ty of some pro­gram­ma­tic design, which may be concrete or ab­stract, in­to a type called a class. As analogy, a class acts like a blueprint from which many in­stan­ces (more po­pu­lar­ly cal­led ob­jects) of the class can be created — much like several houses may be built from the same plan. This analogy applies to any type. Encapsulation, together with inheritance and polymorphism, form the backbone of OOP. Most OOP languages also allow for composition and aggregation as alternatives, or supplementary, to inheritance.

Value Types and Reference Types

The .NET CLR (Common Language Runtime) and CTS (Common Type System), part of the CLI (Common Language Infrastructure), support the concepts of value types and reference types. This was a design choice to enhance efficiency and performance. Most types are ref­e­rence types, cre­at­ed with the class keyword, and in­he­rit from System.Object. In the .NET Frame­work, only a few types, in par­ti­cu­lar the numeric types, are implemented as value types. In C#, they can be created with the struct keyword, and they automatically inherit from a special System.ValueType class.

Value types can be wrapped in a Nullable<T> type. This will allow one to assign null to them, just like references types, for situations where convenience overrules efficiency.

General concepts that involve encapsulation in object-oriented programming, with par­ti­cu­lar ref­e­ren­ce to C# syn­tax and specifics, include:

a) the data an object manages (its state);
b) the manner in which it can be initialised (constructors);
c) the behaviours the object supplies (methods);
d) con­trol of ac­cess to the va­ri­ous parts (access control); and
e) optionally, destruction of an object (finalisation).

Language elements within a class specification are collectively called members. Members are fur­ther ca­te­go­ris­ed as fields, symbolic constants, static fields, constructors, static con­struc­tors, de­struc­tors or finalisers, instance methods, static methods, virtual methods, properties, indexers, and nest­ed types.

Not all these features are implemented on all classes in production, which means the Circle class used as vehicle here, be­comes hea­vi­ly over-complicated when measured against its con­cep­tu­al sim­pli­ci­ty. Remember, this example is about syntax, not good design.

State

The state of an object is effectively the set of values, managed by, and contained with­in an ob­ject, that re­pre­sents the object's memory image at a given time. It is normally implemented as a series of data members, often also called fields. From a design perspective, we want some data members to be constant (read-only or symbolic constant), while some should not be replicated, i.e., exist as a single copy that ‘belongs’ to the class, and is ‘shared’ by all objects.

Read-Only Instance Variables

C# has a specialised readonly modifier, which protects instance variables from being written to after in­i­ti­al­i­sa­tion by a con­struc­tor. They are thus logically constant, but their values can be de­ter­min­ed dy­na­mi­cal­ly (at run-time) in a constructor.

Behaviour

The designed behaviour of an object is facilitated by functions defined within the class. These functions are most often called methods, or more specifically, instance methods. Methods can also be defined as static, in which case we qualify them as static methods.

Properties

Properties are mentioned separately, but technically, they are also just methods with a spe­ci­al syn­tax. They just allow programmers to create the illusion that a member is a variable. Code that uses the class, can use them just like public data members.

Initialisation & Destruction

Practically, client code might like to initialise a new object with particular values during in­stan­ti­a­tion. This is supported via special constructor methods, which may be overloaded. They are spe­cial in that a) their names must be the same as the class name, and b) they can have no re­turn type specified.

Basic Destructor

The C# (optional) shortcut syntax for Finalize() for an arbitrary class, here called MyClass, looks as follows:

   ~MyClass() {
      // de-initialisation code here
      }

The above shortcut is translated by the compiler, to the following code:

   protected override void Finalize() {
      try {
         // de-initialisation code here
         }
      finally {
         base.Finalize(); 
         }
      }

The example Circle class below incorporates a destructor example. It does not really need a destructor; it was only included as an example of the syntax.

Constructor Delegation

It is possible for one constructor to ‘call’ another, which is referred to as ‘con­struc­tor de­le­ga­tion’, or ‘con­struc­tor chain­ing’. This adds a new pattern to the possible syntax for constructors, which adds additional elements between the header and the body of the con­struc­tor meth­od. This allows one to have all the validation in one constructor, while the others simply delegate the initialisation.

Constructor delegation example
class C {
   private int x_, y_;

   public C ()
      : this(1)                          //← delegate to `C(int)` (signature).
      {  }                               //← nothing to do here.

   public C (int x)
      : this(x, 2)                       //← delegate to `C(int,int)`.
      {  }                               //← nothing to do here.

   public C (int x, int y) {             //← ‘main/super’ constructor.
      if (x < 0 || y < 0) {              //← all validation here.
         throw new
            InvalidArgumentException(
               "Bad initialisers"
               );
         }
      x_ = x;
      y_ = y;
      }

   }//class

var c1 = new C();                        // `x_` ⇐  `1`, `y_` ⇐  `2`.
var c2 = new C(11);                      // `x_` ⇐ `11`, `y_` ⇐  `2`.
var c3 = new C(22,33);                   // `x_` ⇐ `22`, `y_` ⇐ `33`.
var c4 = new C(y:33);                    // `x_` ⇐  `1`, `y_` ⇐ `33`.

The arrow above means ‘…gets the value…’.

This is a very common arrangement, although for simpler classes, we could have cre­at­ed con­struc­tors with default arguments instead. However, constructor delegation is much more flexible, and less error prone. Although not necessary, the above example ‘chains‘ from the pa­ra­me­ter­less con­struc­tor to the constructor having one int parameter to the one having two ints as parameters.

Field Initialisers

C# allows for a special (although very natural-looking) syntax to ini­ti­al­ise data mem­bers. It fol­lows the syn­tax for normal local variable initialisation (hence the term: ‘natural-looking’). You should understand that a class is like a house plan: houses can be built from the plan, the plan itself has no space. Thus, the initialisation must take place during construction (run-time). The term field initialiser was chosen as a name for this rule and behaviour.

IMPORTANTField Initialisers

All field initialiser expressions are evaluated, without exception, only when a con­struc­tor is call­ed, and execute before the code in the body of the constructor.

We do not use this syntax in the Circle class, except for the contrived static object_count va­ri­ab­le. But the explanation above does not apply then, since static variables are in­i­ti­alis­ed once, be­fore Main() is cal­led.

Properties

In .NET, ‘property’ is not a generic term. For example, in Visual Basic, it is re­pre­sen­ted with the Property key­word. In C#, it is implemented with a syntactic pattern, that distinguishes it from the pattern for defining methods, or the pattern for defining variables.

Properties are not essential; generally, they just provide simplified, but protected, access to private state variables. In other words, to the client code, a property is used, and acts like, a member variable, which may be readonly.

Read-Only Properties

It is legal to create readonly properties, by simply omitting the set part of the property. This does not involve the readonly modifier. Al­ter­na­tive­ly, you can override the access of the set by prefixing it with private or protected, which means it can still be used inside the class, but client code does not have access.

Since the compiler automatically chooses the name of the ‘backing variable’ represented by the automatic property, we can only access it via the automatic property name.

Access Control

Encapsulation must allow for the concept of privacy, where (especially) data members are pro­tect­ed from client code, so that the integrity (legal range of values for each field) of an object can be controlled by the class code. Most languages implement this in the form of access control. C# implements this with the keywords private, protected, public, and internal.

C# allows for 5 different access control scenarios or levels:

When access control is not explicitly specified, C# will treat it as having private access for mem­bers and internal for classes on the outer level. This applies to all members, not just data mem­bers. Although it is less likely to find methods with private access, it is ne­ver­the­less per­fect­ly le­gal. The same applies to properties, nested types, and even constructors.

Nested Types

For completeness, since it is not used in the example code, understand that inside a class, other classes can be created, hence the term nested. If a class or other type is not inside another, it is said to be on the ‘outer level’. That applies to any user-defined type: struct, enum, interface, etc.

Access to the nested type from code outside the outer class, is just like static members, and access permissions can be controlled with the normal access specifiers.

Overloaded Operators

Overloading an operator is entirely optional, and if not used carefully, can lead to very un­main­tain­able code. Java does not even allow this feature, and C# is more restrictive than C++.

Basic Concepts and Rules

The idea of overloading revolves around that concept that the behaviour of any operator can be model­led with a meth­od (func­tion). In other words: the operands can be thought of as arguments to a function, the operator behaviour is represented by the body of that function, while the re­sult of the operator can be represented with a return statement in the function.

Conceptually then, when you write A + B (where A has type T), then the C# compiler treats it as a static function call: T.operator+(A, B). Since such a function does not exist for user-defined types, it will result in a compilation error, unless of course, you write the method. Here is an ex­tract from the Circle class:

Example addition operator overload
public static Circle operator+ (Circle lhs, Circle rhs) {
   return new Circle(lhs.radius_ + rhs.radius_);
   }

The above code allows us to add two circles, but instead of calling Circle.Add(A, B) directly, for ex­amp­le, we in­di­rect­ly call a similar method (Circle.operator+(A, B)) by writing: A + B, as­sum­ing A and B have type Circle.

All operators eligible for overloading (not many), must be static methods. You cannot create new operators, nor change the precedence or number of operands.

If A and B are different types, you have to implement commutativity yourself. In other words, if you would like A+B to work like B+A, you must overload two + operators with different signatures. For example, if you would like:

    circ1 = circ2 + 0.5;

to be equivalent to:

    circ1 = 0.5 + circ2;

you will have to write the following two overloaded + operators:

public static Circle operator+ (Circle lhs, double rhs) {
   return new Circle(lhs.Radius + rhs);
   }
public static Circle operator+ (double lhs, Circle rhs) {
   return new Circle(lhs + rhs.Radius);
   }

Now you will have commutativity for the addition operator, assuming it is desired.

Compound Assignment

Assuming + is overloaded, C# will automatically supply += (compound assignment). So in­stead of writing: circ1 = circ1 + circ2;, for example, you can write: circ1 += circ2;.

Cast Operators

It is possible to overload the cast operator. For example, we may decide it would be useful to cast a Circle to a double and even the other way around. The operator function must still be static, but cannot have a return type:

public static implicit operator double (Circle c) {
   return c.radius_;
   }
public static explicit operator Circle (double d) {
   return new Circle(d);
   }

The implicit and explicit keywords control whether the compiler will automatically per­form the cast when ne­ces­sa­ry (call the relevant operator function), or only allow it to be cal­led ex­pli­cit­ly with the cast operator.

For the above example, since the cast to double is marked implicit, it means the compiler will au­to­ma­ti­cal­ly create code to call the method when a Circle is used, wher­ever a double was ex­pec­ted. This can obviously be dangerous, so it should be given careful consideration. Rather use the explicit designator when in any doubt.

Encapsulated Circle

Apart from nested types, we employ all these features in a Circle class. We stress again, this is not ne­ces­sa­ri­ly a ‘good’ design for something as simple as the Circle. We simply want to pro­vide you with one example using all the features, instead of several examples using dif­fe­rent fea­tur­es.

To avoid visual noise, we also did not write formal XML comment documentation, which pro­duc­tion code should have.

Circle Client

This is an example of code using our Circle class, i.e., it contains Main(). It does not use all the pro­per­ties and meth­ods, since they follow the same pattern, e.g., getArea() works just like the getCircum() method, so we only illustrated one option.

CircleClient.csCircle Client Program
/*!@file  CircleClient.cs
*  @brief Main() using Circle Class
*/
using System;
using System.Linq;
using System.Collections.Generic;

using static System.Console; //C#6 and up!

namespace Examples { ////

class AppCircleClient {

   const int EXIT_SUCCESS = 0;
   const int EXIT_FAILURE = 1;

   static int Main (string[] args) {

      double radius, area, circum;

      Write("Radius?: ");
      if (!Double.TryParse(ReadLine(), out radius)) {
         Write("Not numeric input. Terminating.");
         return EXIT_FAILURE;
         }

      // Call `Circle(double)` constructor to initialise new object.
      //
      Circle ci = new Circle(radius);

      // Console.WriteLine will call our overridden `ci.ToString()`.
      //
      WriteLine("ci = {0}", ci);
      WriteLine($"ci = {ci}");       //← string interpolation works.

      // Get & set the circle's area by using instance methods.
      //
      area = ci.getArea();
      ci.setArea(32.1);

      // Get & set the circle's area by using the instance property.
      //
      area = ci.Area;
      ci.Area = 32.1;

      // Get & set the circle's area by using the indexer property.
      //
      area = ci["area"];
      ci["area"] = radius * 2.0;

      // Just so we don't get a warning about not using `circum`
      //
      circum = ci.Circum;
      WriteLine("Circumference = {0:N4}", circum);
      WriteLine($"Circumference = {circum:N4}");

      // Test addition and cast operators.
      //
      double dv = ci;               //← implicit cast to double.
      ci = ci + (Circle)dv;         //← overloaded `+` and explicit cast.
      ci += (Circle)dv;             //← you get `+=` for free.

      // Use overridden `ToString()` to print new circle attributes.
      //
      WriteLine("ci = {0}", ci);
      WriteLine($"ci = {ci}");      //← string interpolation works.

      return EXIT_SUCCESS;
      }

   }//AppCircleClient
}////namespace Examples

One could go further and demonstrate arrays of Circles, functions taking a Circle as argument, and functions returning a Circle result.

Exception Handling

We did not use exception handling (trycatch statements) in Main. The only reason for this is to keep the example code cleaner, so that it can better illustrate the use of Circle. But in practice, such an omission in production code would be unforgivable.

Circle Class

Here is the design and implementation of the over-engineered Circle class, which illustrates properties and indexers. It is longer than it needs to be, simply because we show both the setprop() and the getprop() instance methods, and the same behaviour using the cor­res­pon­ding properties (and indexers for good measure).

It also provides a static field, and a static property to provide read-only access to it. This field maintains a count of all Circle objects currently not yet destructed (finalised). It is incremented in the constructors, and decremented in the destructor.

Circle.csCircle Class
/*!@file  Circle.cs
*  @brief Encapsulation Features Employed in a Simple Circle Class.
*/
using System;
using System.Linq;
using System.Collections.Generic;

namespace Examples { ////

public class Circle {

   private double radius_; // the only "state" member

   // Superfluous static data member and associated static property.
   // Property is readonly (only `get`). Use: as `Circle.Count` to
   // see how many circle objects are still alive at a given time.
   //
   private static int object_count_ = 0;

   public static int Count { get => object_count_; }

   // Constructor(s). Destructor added for illustrative purposes, and
   // to decrement the "alive" object count.
   //
   public Circle (double radius = 0.0) {
      if (radius < 0.0)
         throw new ArgumentException("Circle(): Negative radius!");
      radius_ = radius;
      ++object_count_;
      }

   ~Circle () {
      #if DEBUG
         Console.WriteLine("A Circle object being destructed.");
      #endif
      --object_count_;
      }

   // Main ‘behaviour’ methods, i.e. providing the functionality. In
   // less trivial classes, there are generally many more methods. The
   // `this.radius_` in `getCircum()` is not necessary, but included
   // to show that the compiler adds it automatically if not present,
   // since it works without an explict `this.radius_` in `getArea()`
   //
   public double getArea () => Math.PI * radius_ * radius_;
   public double getCircum () => 2.0 * Math.PI * this.radius_;

   // The ‘set’ versions for `Area` and `Circum` simply use the
   // corresponding properties, to show that they are not just for
   // client code.
   //
   public void setArea (double area) => Area = area;
   public void setCircum (double circum) => Circum = circum;

   // ‘Accessor’ methods for the radius, AKA ‘getter & setter’ methods.
   // This is just a common pattern, not a particular syntax.
   //
   public double getRadius () => radius_;

   public void setRadius (double radius) {
      if (radius < 0.0)
         throw new ArgumentException("setRadius(): Negative radius!");
      radius_ = radius;
      }

   // Alternative access to radius, using a property, which is a syntax,
   // re-using the above accessor methods.
   //
   public double Radius {
      get => getRadius();
      set => setRadius(value);  // `value` is automatic parameter name
      }

   // ‘Fake’ Area and Circum properties. Setting the Area or Circum
   // will simply calculate the Radius and set that instead.
   //
   public double Area {
      get => getArea();
      set => setRadius(Math.Sqrt(value / Math.PI));
      }

   public double Circum {
      get => getCircum();
      set => setRadius(value / Math.PI / 2.0);
      }

   // Totally superfluous indexer property for illustration. We could
   // have used `return radius_;` instead for `case "RADIUS"`, but by
   // design wanted to have only one "exit point" for the radius in
   // all the code. Alternatively, we could have simply used the above
   // property: `return Radius;`. The same applies to Area, and Circum:
   // we could have used: `return Area;` or `return Circum;` instead.
   // But those properties call `set/getArea()` and `set/getCircum()`
   // anyway, so this is actually more efficient (by a small margin).
   //
   public double this[string index] {
      get{
         switch (index.ToUpper()) {
            case "RADIUS": return getRadius();
            case "CIRCUM": return getCircum();
            case "AREA"  : return getArea();
            default:
               throw new ArgumentOutOfRangeException(
                  "Circle[].get: Invalid index");
            }
         }
      set{
         switch (index.ToUpper()) {
            case "RADIUS": setRadius(value); break;
            case "CIRCUM": setCircum(value); break;
            case "AREA"  : setArea(value);   break;
            default:
               throw new IndexOutOfRangeException(
                  "Circle[].set: Invalid index");
            }
         }
      }

   // Overloaded addition and cast operators
   //
   public static Circle operator+ (Circle lhs, Circle rhs) =>
      new Circle(lhs.radius_ + rhs.radius_);
   public static implicit operator double (Circle c) => c.radius_;
   public static explicit operator Circle (double d) => new Circle(d);

   // Bonus: overriding `ToString()`, which `Circle` inherits from
   // `System.Object` (automatically), so it returns something useful.
   //
   public override string ToString() =>
      String.Format(
         "[R={0:N4},C={1:N4},A={2:N4}]", Radius, Circum, Area
         );

   }/*Circle*/
}////namespace Examples

TIPString Formatting

All string formatting in the .NET Framework classes —not just conversion to a string by virtue of override'ing the inherited ToString method from object— is performed by one of the se­ve­ral ov­er­load­ed Format methods of the string class. Should you want similar behaviour for your classes, you will have to implement the IFormattable interface.

Summary

Encapsulation is useful for any program, and does not have to involve the other aspects of ob­ject-ori­en­ted pro­grams, like inheritance and polymorphism (virtual methods). Combining only encapsulation and modularisation (using static classes with static functions), much can be accomplished in a readable and maintainable way. For example, Visual Basic, until version 6, only offered modularisation and encapsulation, and many successful programs were written with it.

Overloading operators should be carefully considered. When in doubt, do not over­load any.

This fact is often overlooked, and should thus be stressed. You can create useful code without it being full-blown object-oriented. If you add interfaces and simple generics to the list, it pro­mot­es even better re-use, and still does not require in-depth OOAD (Object-Oriented Analysis & Design).

Interface Implementation

The term implement an ‘interface’ refers to the programming concept of interfaces, and a syntax whereby a class can ‘implement an interface’. This is often used as an alternative to, or in ad­dition to, inheritance in object-oriented languages. We could have implemented some minor inter­face in the Circle class example, but decided that it was slightly out of scope, considering our in­tend­ed au­di­en­ce, and would have increased the code even further, which may have ob­scured the syntax we wanted to highlight.

But for the curious, we provide some snippets you can add to Circle. Let us assume you want to be able to write code like this, using a Circle, where you want to choose which of the at­tri­bu­tes: radius (:R), circumference (:C) or area (:A) to format as a string:

   // format the area of a `Circle` using `:A` as format specifier.
   //
   ⋯string.Format("{0:A}", circ1);       // or...
   WriteLine("{0:A}", circ1);            // (`Write/Line` calls `Format`);
   WriteLine($"{circ1:A}");              // same for string interpolation.

In such a situation, some overloaded versions of String.Format will expect objects that im­ple­ment the IFormattable interface. This interface dictates overloading a specific ToString method. This means Circle must implement it, for the above code to work:

public class Circle : IFormattable {

That, however, is not enough, but does mean this class promises to implement the ‘interface’. For the class to uphold the promise, it must create a function that looks as follows:

public string ToString (string format, IFormatProvider formatProvider);

The code will not compile if this method is not defined (implemented). Our implementation will ignore the second parameter, for the sake of simplicity:

public class Circle : IFormattable {

   public string ToString (string fmt, IFormatProvider fp) {
      switch (fmt) {
         case "R" : return getRadius().ToString("N4");
         case "C" : return getCircum().ToString("N4");
         case "A" : return getArea()  .ToString("N4");
         }
      return "*ERROR*";
      }

   }//class

And that is it: code like Write("{0}:A", circ1) will now return the area. Instead of the :A, you can use :C for circumference, or :R for the radius. This is clearly not necessary, but like indexers, this is an option that you should at least be aware of. The decision whether it is legitimately useful or not, is another matter, and depends on the complexity of the class in question.

You now have an example of ‘implementing an interface’. This will always involve look­ing up, and implementing, the methods that the interface represents. This is about as far as you can take en­cap­su­la­tion, without resorting to inheritance and polymorphism.


2021-04-01: Links for .NET API 5.0 & using expression-bodied function syntax. [brx]
2020-03-19: Minor fixes; string interpolation, virtual properties. [brx]
2017-11-30: Editing. [brx;jjc]
2017-11-25: Additional topics. Rephrasing. Reorganisation. Editing. [brx;jjc]
2017-11-23: Rephrasing, editing, new admonition syntax and many links. [brx]
2016-12-01: Created. [brx]