Skip to content

Chapter 17: Objects & State - Fields, Properties, and Encapsulation

Theoretical Foundations

Theoretical Foundations: Fields, Properties, and Encapsulation

Imagine you are building a simple AI chatbot. This chatbot needs to remember the user's name to personalize responses. Where does it store that name? In a variable? But variables inside a method disappear when the method finishes running. We need a place to store data that lives as long as the chatbot instance itself. This brings us to the core of object-oriented programming: managing state.

In Chapter 16, we learned how to define a class and create an instance of it using the new keyword. We defined methods (behavior) but didn't store any data specific to that instance. Now, we will learn how to give our objects memory.

1. Fields: The Memory of an Object

A field is a variable declared directly within a class. It holds the state of an object. Think of a class as a blueprint for a house. The fields are the specific rooms and furniture inside that house. When you build two houses (create two instances) from the same blueprint, they each have their own rooms.

Declaring Fields Fields are declared inside the class body but outside any method. We use the same syntax as declaring local variables (Chapter 2), but we usually add an access modifier like private (more on this later).

using System;

public class ChatBot
{
    // This is a field. It stores the bot's memory.
    // It is accessible anywhere inside the ChatBot class.
    private string userName;

    // This is a method we defined in Chapter 13.
    public void GreetUser()
    {
        // We can access the field 'userName' here.
        Console.WriteLine($"Hello, {userName}!");
    }
}

Initializing Fields When you create an instance of a class, its fields are initialized to default values (e.g., 0 for integers, null for strings). However, we often want to set an initial value immediately. We can do this using an initializer.

public class ChatBot
{
    // Initializing the field at the point of declaration.
    private string userName = "Guest";
    private int messageCount = 0;
}

The this Keyword When a local variable (like a parameter) has the same name as a field, the local variable "hides" the field. To explicitly refer to the field, we use the this keyword. this refers to the current instance of the class.

public class ChatBot
{
    private string userName;

    // The parameter 'name' hides the field 'userName'.
    // We use 'this.userName' to distinguish the field from the parameter.
    public void SetUserName(string name)
    {
        this.userName = name; 
    }
}

2. Properties: Controlled Access

In the previous example, we declared userName as private. This means code outside the ChatBot class cannot access it directly. This is a fundamental rule of C#: Encapsulation. We hide the internal state of an object to prevent accidental corruption.

However, we still need a way for external code to read or update that state safely. This is where Properties come in. A property looks like a field from the outside but acts like a method on the inside. It controls access to the field using accessors: get and set.

Auto-Implemented Properties For simple cases where we don't need extra logic, C# provides a shorthand called auto-implemented properties. This creates a hidden backing field automatically.

public class ChatBot
{
    // This looks like a field, but it's a property.
    // The compiler creates a hidden private field to store the value.
    // We can access this property from outside the class.
    public string UserName { get; set; }
}

Backing Fields and Logic Sometimes we need to add logic when getting or setting a value. For example, we might want to ensure a user's age is never negative. To do this, we declare a backing field (a private field) and write custom logic in the accessors.

public class ChatBot
{
    // The private field that actually stores the data.
    private int _messageCount;

    // The public property that controls access.
    public int MessageCount
    {
        get
        {
            // We can add logic here if needed (e.g., logging).
            return _messageCount;
        }
        set
        {
            // 'value' is a keyword representing the value being assigned.
            if (value >= 0)
            {
                _messageCount = value;
            }
            else
            {
                Console.WriteLine("Message count cannot be negative.");
            }
        }
    }
}

Read-Only and Write-Only Properties We can restrict access by omitting one of the accessors.

  • Read-Only: get only. Useful for IDs calculated once.
  • Write-Only: set only. Rare, but useful for passwords where you can set but never read back.
public class ChatBot
{
    private string _apiKey;

    // Read-Only property (can only be read, not changed after creation)
    public string BotVersion { get; } = "1.0.0";

    // Write-Only property (can only be set, not read)
    public string ApiKey
    {
        set { _apiKey = value; }
    }
}

3. Encapsulation: The Protective Shield

Encapsulation is the practice of bundling data (fields) and methods that operate on that data (properties and methods) together, and restricting direct access to some of the object's components.

Why Encapsulate?

  1. Data Integrity: By using properties, we validate data before assigning it. We prevent invalid states (e.g., a temperature of -500 Kelvin).
  2. Flexibility: We can change how data is stored internally without breaking external code. If we change a field from int to double, the property handles the conversion, and the code using the property doesn't need to change.
  3. Security: We hide implementation details. External code shouldn't know how the chatbot stores messages, only that it can store them.

Access Modifiers Access modifiers control visibility:

  • public: Accessible from anywhere.
  • private: Accessible only within the same class (default for class members if not specified).
  • protected: Accessible within the class and by derived classes (we will cover this in inheritance chapters).

Real-World Analogy: The Car Dashboard Think of a car.

  • Fields: The internal engine temperature, fuel level, and tire pressure. These are raw data points.
  • Properties (Getters): The dashboard gauges. You look at the speedometer to read the speed. You don't touch the engine wires directly.
  • Properties (Setters): The gas pedal and steering wheel. You press the pedal (set the fuel injection), but the car ensures you don't inject too much fuel at once (logic in the setter).
  • Encapsulation: You cannot directly access the fuel injection system while driving. The dashboard protects you from the complexity and danger of the internal mechanics.

4. Immutability and State Management

In AI development, managing state is critical. If an AI model is training, its state (weights, loss) changes constantly. However, sometimes we want objects that represent fixed data, like configuration settings.

Read-Only Properties for Immutability By using { get; } without a set, we create a read-only property. This makes the object immutable (unchangeable) after creation.

public class ModelConfig
{
    // This can only be set in the constructor (Chapter 16 context)
    // but read anywhere.
    public string ModelName { get; }
    public double Temperature { get; }

    public ModelConfig(string name, double temp)
    {
        ModelName = name;
        Temperature = temp;
    }
}

Why is this useful for AI? When passing configuration objects to an AI function, you want to ensure the configuration doesn't change halfway through processing. By making properties read-only, you guarantee the state remains consistent during the execution of the AI logic.

5. Connecting to AI Development

In the context of building AI applications (like the chatbot mentioned earlier), fields and properties are the backbone of state management.

Example: A Simple AI Context An AI chatbot often needs to maintain a "context" or "memory" of the conversation. We can use a class to hold this state.

public class ConversationContext
{
    // Field to store the history (using an array from Chapter 11)
    // We use a private field to hide the complexity.
    private string[] _history = new string[10];
    private int _messageIndex = 0;

    // Property to expose the count of messages
    public int MessageCount 
    { 
        get { return _messageIndex; } 
    }

    // Method to add a message (Behavior)
    public void AddMessage(string message)
    {
        if (_messageIndex < 10)
        {
            _history[_messageIndex] = message;
            _messageIndex++;
        }
    }

    // Method to retrieve the last message
    public string GetLastMessage()
    {
        if (_messageIndex > 0)
        {
            return _history[_messageIndex - 1];
        }
        return "No messages.";
    }
}

Swapping Models (The Interface Connection) While we haven't covered Interfaces yet (that's a future chapter), properties allow us to prepare for advanced architecture. Imagine we want to swap between a "Local AI Model" and an "OpenAI Model".

Both models need to store a ModelName and Temperature. By defining these as properties in a base class, we can treat both models interchangeably. The property acts as a contract: "Any AI model must have a Temperature I can read."

// Conceptual example of how properties enable polymorphism
public class AIModel
{
    public string ModelName { get; set; }
    public double Temperature { get; set; }

    public virtual void GenerateResponse(string prompt) 
    { 
        // Base implementation
    }
}

// Later, we can create specific models
public class LocalModel : AIModel { /* ... */ }
public class OpenAIModel : AIModel { /* ... */ }

6. Visualization of Object State

The following diagram illustrates how an instance of a class exists in memory (Heap) and how fields and properties relate to it.

7. Summary of Concepts

  1. Fields are variables declared within a class. They hold the raw data (state) of an object.
  2. Properties provide controlled access to fields using get and set accessors.
  3. Encapsulation protects the internal state of an object from unintended external modification.
  4. Auto-implemented properties ({ get; set; }) are shorthand for properties with hidden backing fields.
  5. Backing fields are explicitly declared private fields used when custom logic is needed in getters or setters.
  6. Read-only properties ({ get; }) promote immutability, which is crucial for stable configuration in AI systems.

By mastering these foundations, you ensure that your AI applications have a robust memory system, capable of storing and protecting the data necessary for intelligent behavior.

Basic Code Example

Here is a simple, 'Hello World' level code example for the concept of Fields and Properties.

using System;

// A class represents a real-world entity.
// Here, we define a 'Car' that needs to track its speed.
public class Car
{
    // 1. FIELD DECLARATION
    // A field is a variable declared directly inside a class.
    // We use 'private' to restrict access to only code inside this class.
    // This protects the data from being changed incorrectly.
    private int _currentSpeed;

    // 2. PROPERTY DECLARATION
    // A property provides controlled access to a field.
    // It uses 'get' and 'set' accessors.
    public int Speed
    {
        // The 'get' accessor allows reading the value.
        get { return _currentSpeed; }

        // The 'set' accessor allows writing a value.
        // 'value' is a keyword representing the data being assigned.
        set { _currentSpeed = value; }
    }

    // A method to demonstrate behavior
    public void DisplayStatus()
    {
        Console.WriteLine($"The car is traveling at {Speed} mph.");
    }
}

public class Program
{
    public static void Main()
    {
        // 3. CREATING AN INSTANCE
        // We create an object of the Car class using 'new'.
        Car myCar = new Car();

        // 4. USING THE PROPERTY
        // We can assign a value to the 'Speed' property.
        // This actually calls the 'set' accessor in the Car class.
        myCar.Speed = 60;

        // 5. READING THE PROPERTY
        // We read the value using the 'Speed' property.
        // This calls the 'get' accessor.
        Console.WriteLine($"Current speed: {myCar.Speed} mph");

        // 6. CALLING A METHOD
        // We use the object's method to display the status.
        myCar.DisplayStatus();

        // 7. MODIFYING THE STATE
        // Let's change the speed.
        myCar.Speed = 75;
        Console.WriteLine("Accelerating...");
        myCar.DisplayStatus();
    }
}

Code Breakdown

  1. The Car Class Definition: We define a class named Car. Think of a class as a blueprint. This blueprint tells the computer what data a car has (fields) and what a car can do (methods).
  2. The Field (_currentSpeed): Inside the class, we declare a private integer _currentSpeed. The underscore is a common naming convention for private fields. This variable holds the actual data. We mark it private so that outside code cannot directly mess with it (e.g., setting it to a negative number without checks).
  3. The Property (Speed): The public int Speed is the interface for the outside world. It looks like a variable but acts like a gatekeeper.
    • get: When someone asks for myCar.Speed, this code runs and returns the value of _currentSpeed.
    • set: When someone assigns a value like myCar.Speed = 60, this code runs. The keyword value holds the number 60, and we assign it to _currentSpeed.
  4. Instantiation (new Car()): In the Main method, we create an actual object (instance) of the Car blueprint. myCar is a variable that holds this specific car.
  5. Property Access: We interact with myCar using the Speed property. We don't touch _currentSpeed directly. This is the core of encapsulation—using the property to manage the field.
  6. State Change: By assigning different values to Speed, we change the state of the object. The DisplayStatus method reads the current state to report it.

Common Pitfalls

Confusing Fields and Properties A frequent mistake for beginners is trying to use the field _currentSpeed outside the class. Because we declared it as private, the following line in Main would cause a compiler error:

// ERROR: The field 'Car._currentSpeed' is inaccessible due to its protection level.
Console.WriteLine(myCar._currentSpeed);

The Fix: Always use the public Speed property to read or write data from outside the class. The field is for internal storage; the property is for public access.

The chapter continues with advanced code, exercises and solutions with analysis, you can find them on the ebook on Leanpub.com or Amazon



Code License: All code examples are released under the MIT License. Github repo.

Content Copyright: Copyright © 2026 Edgar Milvus | Privacy & Cookie Policy. All rights reserved.

All textual explanations, original diagrams, and illustrations are the intellectual property of the author. To support the maintenance of this site via AdSense, please read this content exclusively online. Copying, redistribution, or reproduction is strictly prohibited.