Classes in Detail
We will now focus on the usage of classes. Object-oriented programming organizes code and data into objects. As we have seen, classes can contain fields, properties, constructors, and methods with various access modifiers.
- Fields - These are the entries in the object's structure as they exist in the program's memory. They are written in Pascal-Case, except when they are private, in which case they are written with "_" in front and the rest in Camel-Case.
- Methods - These are the functions associated with the class. They are written in Pascal-Case.
- Constructors - These are special functions (with variations in interpretation depending on the programming language) that are called when an object is created after the memory area has been allocated using the new keyword to instantiate the object. They are written in Pascal-Case.
- Properties - These are special methods without explicit parameters that are accessed like fields but are actually internally invoked as functions. Properties are equivalent to getters and setters in Java or other languages, but they are unified under a unique form. Properties have the advantage of encapsulating computations that can be performed on variable setting or reading, and they may or may not have backing fields. They are written in Pascal-Case.
Below is an example of a class with different fields, properties, and methods with access modifiers and keywords.
/*
* "internal" for classes and fields makes the class visible only within the current assembly.
*/
internal class TestClass {
// "public" makes the class visible anywhere; "const" for primitives is equivalent to "static readonly".
public const int Constant = 42;
/* "readonly" allows a field to be initialized only by a constructor and prevents further changes.
* "private" makes this field visible only to the containing class.
*/
private readonly string _privateField;
// You can create properties, which are methods/functions that act like members but are computed each time they're read.
public string DoubledProperty => _privateField + _privateField;
// You can create properties with implicit getter and setter to get and set values without an explicitly declared field.
public int SecondProperty { get; set; }
private string _backedField;
/* You can create properties that set private fields and involve a computation on a backed field.
* The "value" keyword in the setter represents the value provided during assignment.
*/
public string ThirdProperty { get => _backedField; set => _backedField = value + value; }
public TestClass(string privateField)
{
_privateField = privateField;
}
/* As a non-static method, there's access to the class's fields, methods, and properties,
* and you can refer to the instance itself using "this".
*/
public TestClass ConcatStringAndReturn(string other)
{
return new TestClass(this._privateField + other);
}
/* For any method, you can have an overload - a method with the same name
* but a different parameter list with distinct types.
*/
public TestClass ConcatStringAndReturn(TestClass other)
{
return new TestClass(this._privateField + other._privateField);
}
/* In a static method marked with the "static" keyword, you can reference private class fields
* but you can't use the "this" keyword, as a static method isn't bound to an object instance.
*/
public static TestClass ConcatClasses(TestClass a, TestClass b)
{
return new TestClass(a._privateField + b._privateField);
}
}
Access Modifiers
There are various access modifiers for classes, methods, and fields. Access modifiers are used to restrict access from different parts of the code. Essentially, programmers ensure that the data within a class cannot be accessed elsewhere in the code to prevent errors. The main access modifiers are:
- public – anything with this access modifier is visible throughout the source code.
- protected – a class or member in the class can only be accessed by classes that inherit from it (see inheritance).
- private – a class or its members can only be seen by itself.
- internal – also known as default; if no access modifier is explicitly provided, this is applied. This makes the class or its members visible only within its own assembly, i.e., its own executable or the library resulting from compilation.
A summary of access modifiers is as follows:
Access Location | public | protected internal | protected | internal | private protected | private |
---|---|---|---|---|---|---|
Inside class | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
Derived class (same assembly) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ❌ |
Non-derived class (same assembly) | ✔️ | ✔️ | ❌ | ✔️ | ❌ | ❌ |
Derived class (different assembly) | ✔️ | ✔️ | ✔️ | ❌ | ❌ | ❌ |
Non-derived class (different assembly) | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ |
Access modifiers must be used to prevent incorrect usage of objects. If there's a scenario where accessing a class or a member could lead to an error, attaching an access modifier can eliminate such scenarios.
Static vs. Non-Static
The keyword static has two uses. Applied to classes, it makes that class only instantiable during the program's initialization by the framework and enforces that fields and methods within the class are static. Applied to fields or methods, it makes them usable by all object instances, even outside the class. The "this" keyword cannot be used here to reference an object instance.
When discussing static and non-static methods, we're referring to whether a method has access to an object instance using the "this" keyword. Generally, the difference between the two is said to be that they have different contexts - one non-static that varies by object instance and one static that's the same always. Contrary to this widespread conception, the truth is that there's no difference between the two. Methods are functions that exist in the same code region; the difference is that for non-static methods, the compiler adds, without the programmer noticing, the first parameter to the non-static function of the class's type, representing the object instance through which the method is called - this first parameter is the "this".
When to use static and non-static fields and methods:
- No need to reference the own instance
- When constants need to be visible throughout the code
- When traditional functions are needed
When to use non-static fields and methods:
- Need to keep variables in an object to track state
- When using the data from the same object for multiple computations
Immutability
It should be noted here that objects can be immutable, meaning their internal state doesn't change after creation. To modify the object, a new instance is always created with the modification. An example of an immutable object is the string type; if you concatenate a string with another, a new instance is created, while the previous one remains the same. Although immutability might consume more memory, it's desired in many scenarios. For instance, in a predominantly functional codebase, immutable objects are suitable as side effects can negatively affect the program's flow. Similarly, in a multi-threading context, if objects are only read, there's no need for synchronization methods between threads, which positively impacts program performance as these mechanisms are more expensive and simplify the program's flow by avoiding them.
Overloading
An important term in OOP is that methods can be overloaded, meaning multiple methods, static or non-static, with the same name but different signatures can exist in the same class. A specific method with a specific parameter set is called an overload of that function; this applies to constructors as well.
Exercises:
- Create a singly linked list using classes with values of type int and string, having operations for adding, deleting, and accessing by index.
- Create a doubly linked list using classes with values of type int and string, having operations for adding, deleting, and accessing by index.
- Discuss what could improve the solutions to eliminate duplicate code.