Iron Academy Logo
Learn C#

Understanding C# Access Modifiers

Tim Corey
46m 57s

In this article, we will dive into C# access modifiers, which define the visibility and accessibility of types, methods, and variables in C#. Tim Corey in his video "C# Access Modifiers (beyond public and private) - what they are, how to use them, and best practices", explains various access modifiers and demonstrates their practical usage in a console application.

This article will explain what Tim covered, along with code examples to enhance your understanding. The timestamps provided allow you to follow along with the video for a more hands-on experience

What Are Access Modifiers?

Introduction

Tim Corey starts by introducing access modifiers, explaining that they determine who can see and use a resource in C#. While we are familiar with the commonly used public and private modifiers, Tim explores six different access modifiers and their use cases.

Demo Application Explained

Tim sets up a simple application to demonstrate how various access modifiers work. The application consists of a console user interface and a demo library, both in .NET framework.

Project Structure:

  • Console UI: A console application to test access modifiers.

  • Demo Library: A class library where different access modifiers are demonstrated.
public class AccessDemo
{
    private void PrivateDemo() { }
    internal void InternalDemo() { }
    public void PublicDemo() { }
}
public class AccessDemo
{
    private void PrivateDemo() { }
    internal void InternalDemo() { }
    public void PublicDemo() { }
}

1. Private

What It Is:

The private modifier restricts access to the method, field, or property only private members within the class where it is defined.

Code Example:

public class AccessDemo
{
    private void PrivateDemo()
    {
        Console.WriteLine("Private method can only be accessed within this class.");
    }

    public void CallPrivateDemo()
    {
        PrivateDemo();  // Works because it's within the same class
    }
}
public class AccessDemo
{
    private void PrivateDemo()
    {
        Console.WriteLine("Private method can only be accessed within this class.");
    }

    public void CallPrivateDemo()
    {
        PrivateDemo();  // Works because it's within the same class
    }
}

Explanation: The PrivateDemo method is only accessible within the AccessDemo class. In the video, Tim demonstrates that it cannot be accessed from outside the class, even if other classes are in the same project.

Best Practice: Use private when you want to restrict access to the internal workings of your class, ensuring that it cannot be directly altered from other parts of your application.

2. Internal

What It Is:

The internal modifier allows access to the method or property only within the same assembly (the project). This is broader than the private access modifier, as it includes all classes within the same project.

Code Example:

public class AccessDemo
{
    internal void InternalDemo()
    {
        Console.WriteLine("Internal method is accessible within the same assembly.");
    }
}

Explanation: The InternalDemo method can be accessed by any two class members from within the same assembly, but not from other assemblies. In the video, Tim shows that internal allows access inside the same project but denies access from outside.

Best Practice: Use internal for methods or properties that are meant to be used only within the current assembly itself, such as helper functions or utilities that should not be exposed to external projects.

3. Public

What It Is:

The public modifier allows access to the method or property from any other class or assembly. This is the most permissive access level.

Code Example:

public class AccessDemo
{
    public void PublicDemo()
    {
        Console.WriteLine("Public method can be accessed from any class.");
    }
}
public class AccessDemo
{
    public void PublicDemo()
    {
        Console.WriteLine("Public method can be accessed from any class.");
    }
}

Explanation: The PublicDemo method is accessible from anywhere, including other classes in the same assembly or other assemblies. Tim demonstrates that public is the most common access modifier, especially when exposing methods in libraries.

Best Practice: Use public for methods and properties that need to be accessible by other parts of the application or external projects, such as API endpoints or widely used utilities.

4. Protected

What It Is:

The protected modifier allows access to the method or property within the parent class where it is defined, and in any derived classes (inheritance). This modifier is useful for object-oriented programming, especially in cases of inheritance.

Code Example:

public class AccessDemo
{
    protected void ProtectedDemo()
    {
        Console.WriteLine("Protected method can be accessed within the class and derived classes.");
    }
}

public class DerivedClass : AccessDemo
{
    public void CallProtectedDemo()
    {
        ProtectedDemo();  // Accessible because of inheritance
    }
}
public class AccessDemo
{
    protected void ProtectedDemo()
    {
        Console.WriteLine("Protected method can be accessed within the class and derived classes.");
    }
}

public class DerivedClass : AccessDemo
{
    public void CallProtectedDemo()
    {
        ProtectedDemo();  // Accessible because of inheritance
    }
}

Explanation: The ProtectedDemo method can be accessed from the AccessDemo class and any class that inherits from it. Tim explains that protected is less common but very useful when working nested classes with inheritance.

Best Practice: Use protected when you want to allow derived classes to have access to specific methods or properties, but do not want them to be accessible outside of protected members of the class hierarchy.

5. Private Protected

What It Is:\ The private protected modifier is a specialized access modifier that combines the rules of private and protected. It restricts access to methods or properties within the defining class and derived classes within the same assembly. This means it offers a further protection level and tighter boundary for inheritance-based access control compared to protected.

Code Example:

public class AccessDemo
{
    private protected void PrivateProtectedDemo()
    {
        Console.WriteLine("Private Protected method can be accessed within the same assembly and derived classes.");
    }
}

public class DerivedClass : AccessDemo
{
    public void CallPrivateProtectedDemo()
    {
        PrivateProtectedDemo();  // Accessible because of inheritance within the same assembly
    }
}

public class UnrelatedClass
{
    public void TestAccess()
    {
        // PrivateProtectedDemo();  // Error: Not accessible in unrelated classes
    }
}
public class AccessDemo
{
    private protected void PrivateProtectedDemo()
    {
        Console.WriteLine("Private Protected method can be accessed within the same assembly and derived classes.");
    }
}

public class DerivedClass : AccessDemo
{
    public void CallPrivateProtectedDemo()
    {
        PrivateProtectedDemo();  // Accessible because of inheritance within the same assembly
    }
}

public class UnrelatedClass
{
    public void TestAccess()
    {
        // PrivateProtectedDemo();  // Error: Not accessible in unrelated classes
    }
}

Explanation: The PrivateProtectedDemo method is accessible in the DerivedClass because it inherits from AccessDemo and exists in the same assembly. However, it cannot be accessed in UnrelatedClass, as this class does not inherit from AccessDemo. Furthermore, if AccessDemo is part of a different assembly, even derived classes containing class like InheritanceDemo would not have access to PrivateProtectedDemo.

Tim demonstrates this difference by showing that protected allows access in derived classes across assemblies, while private protected limits access strictly to the same in class program assembly.

Best Practice: Use the private protected access modifier sparingly when you need to tightly control inheritance access within the same assembly. This is especially useful in scenarios where exposing methods or properties across assemblies might compromise encapsulation. However, default to simpler access modifiers like private or protected unless specific needs arise.

6. Protected Internal

What It Is:

The protected internal modifier combines the protected and the internal access modifier two levels. It allows access from the same assembly or from derived classes, even if they are in another assembly.

Code Example:

public class AccessDemo
{
    protected internal void ProtectedInternalDemo()
    {
        Console.WriteLine("Protected Internal method can be accessed within the same assembly or from derived classes.");
    }
}

public class DerivedClass : AccessDemo
{
    public void CallProtectedInternalDemo()
    {
        ProtectedInternalDemo();  // Accessible due to inheritance
    }
}
public class AccessDemo
{
    protected internal void ProtectedInternalDemo()
    {
        Console.WriteLine("Protected Internal method can be accessed within the same assembly or from derived classes.");
    }
}

public class DerivedClass : AccessDemo
{
    public void CallProtectedInternalDemo()
    {
        ProtectedInternalDemo();  // Accessible due to inheritance
    }
}

Explanation: The ProtectedInternalDemo method is accessible from within the same assembly, and also from any derived class, regardless of the assembly. Tim demonstrates this modifier when explaining how it allows access both inside the same assembly and across inheritance boundaries.

Best Practice: Use protected internal when you want child class to expose a method to both derived classes (in other assemblies) and classes in the same assembly, but not to everyone.

Why Not Make Everything Public?

Tim Corey explains the importance of using access modifiers and why everything shouldn't just be public. While making everything public might seem convenient, it introduces significant risks, including data breaches, bugs, and confusion in development. Access modifiers exist to secure private information, prevent unintended public access modifier used, and provide clarity in codebases.

1. Protecting Private Information

Tim discusses why sensitive information, such as Social Security Numbers (SSNs) or credit card numbers, should not be made public. He demonstrates a "bad class" example where data exposure occurs due to public access:

Bad Example:

public class User
{
    public string SSN; // Anyone can access and modify it directly
}
public class User
{
    public string SSN; // Anyone can access and modify it directly
}

Good Example:

public class User
{
    private string ssn;

    public string GetMaskedSSN()
    {
        return "XXX-XX-" + ssn.Substring(ssn.Length - 4);
    }

    public void SetSSN(string value)
    {
        // Add validation if needed
        ssn = value;
    }
}
public class User
{
    private string ssn;

    public string GetMaskedSSN()
    {
        return "XXX-XX-" + ssn.Substring(ssn.Length - 4);
    }

    public void SetSSN(string value)
    {
        // Add validation if needed
        ssn = value;
    }
}

If everything is public, it wouldn't matter that this is trying to filter out certain data... it bypasses those protections.

2. Securing Private Methods

Starting: 35:11\ Tim explains that private methods help encapsulate behaviors that should not be directly accessible. He uses the example of a DeleteUser method as part of a larger process like employee offboarding.

Bad Example:

public class UserManager
{
    public void DeleteUser(int userId)
    {
        // Deletes the user without considering related processes
    }
}
public class UserManager
{
    public void DeleteUser(int userId)
    {
        // Deletes the user without considering related processes
    }
}

Good Example:

public class UserManager
{
    public void OffboardUser(int userId)
    {
        RevokeAccess(userId);
        DeleteUser(userId); // Used privately as part of offboarding
    }

    private void DeleteUser(int userId)
    {
        // Internal logic to delete the user
    }

    private void RevokeAccess(int userId)
    {
        // Logic to revoke system access
    }
}
public class UserManager
{
    public void OffboardUser(int userId)
    {
        RevokeAccess(userId);
        DeleteUser(userId); // Used privately as part of offboarding
    }

    private void DeleteUser(int userId)
    {
        // Internal logic to delete the user
    }

    private void RevokeAccess(int userId)
    {
        // Logic to revoke system access
    }
}

3. Preventing Bugs

Access modifiers prevent bugs by ensuring data is set or retrieved with proper validation. Tim illustrates this with an example involving an Age property.

Bad Example:

public class Person
{
    public int Age; // Can be directly set to an invalid value
}
public class Person
{
    public int Age; // Can be directly set to an invalid value
}

Good Example:

public class Person
{
    private int age;

    public int Age
    {
        get { return age; }
        set
        {
            if (value < 0 || value > 120)
                throw new ArgumentOutOfRangeException("Age must be between 0 and 120.");
            age = value;
        }
    }
}
public class Person
{
    private int age;

    public int Age
    {
        get { return age; }
        set
        {
            if (value < 0 || value > 120)
                throw new ArgumentOutOfRangeException("Age must be between 0 and 120.");
            age = value;
        }
    }
}

You should never have public backing fields... it introduces bugs by bypassing the checks.

4. Reducing Confusion and Enhancing Clarity

Proper use of access modifiers simplifies development by exposing only what is necessary, avoiding confusion. For instance, in an application with thousands of methods, exposing only the public ones ensures developers see only the relevant options.

Example:

public class MathLibrary
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;

    private void LogCalculation(string operation, int result)
    {
        // Logging is internal and not exposed
    }
}
public class MathLibrary
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;

    private void LogCalculation(string operation, int result)
    {
        // Logging is internal and not exposed
    }
}

In this case, StartEngine and StopEngine are public because they are meant for external use, while CheckFuel and CheckBattery remain private for internal operations. It cleans up for us... you only have access to the things you should and can use.

5. Benefits in Larger Applications or Libraries

For large-scale projects, Tim explains how proper use of access modifiers ensures only the required parts of internal class of a library are exposed, reducing the cognitive load for developers using the library.

Example:

public class MathLibrary
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;

    private void LogCalculation(string operation, int result)
    {
        // Logging is internal and not exposed
    }
}
public class MathLibrary
{
    public int Add(int a, int b) => a + b;
    public int Subtract(int a, int b) => a - b;

    private void LogCalculation(string operation, int result)
    {
        // Logging is internal and not exposed
    }
}

By using these access modifiers correctly, you make your job and the next person’s job easier.

Conclusion

Tim Corey provides a clear and practical guide to mastering C# access modifiers, showing how to use them effectively to create secure, maintainable, and professional applications. His detailed explanations and real-world examples make this topic accessible for developers of all levels.

For more in-depth insights and to see these concepts in action, be sure to watch Tim's full video and explore his channel for valuable content on C# and other programming topics. It’s a must-visit resource for anyone serious about improving their development skills!