Question

C# Access Modifiers and static: public, private, protected, and Defaults

csharp.netasp.netaccess-modifiers

Question

In C#, what are the differences between the public, private, and protected access modifiers, and what happens when no access modifier is written?

I have mostly used public throughout college and want to understand when each access level should be used. Also, how does the static keyword differ from leaving it off a member?

Short Answer

You will learn how C# access modifiers control who can use a type or member, what default accessibility means, and why static is a separate concept from accessibility. You will also see when to use instance members versus class-level static members.

Concept

Access modifiers define accessibility: the parts of a program that are allowed to use a type, field, method, property, or constructor.

  • public means accessible from any code that can access the containing type or assembly.
  • private means accessible only inside the type that declares it.
  • protected means accessible inside the declaring type and inside types derived from it.
  • Omitting an access modifier uses a C# default that depends on where the declaration appears.

For members declared directly in a class or struct, the default is private:

class Account
{
    string ownerName; // private by default
}

For a top-level type declared directly in a namespace, the default is internal, meaning it is available anywhere in the same assembly (usually the same project output), but not from another assembly:

class ReportGenerator // internal by default
{
}

static is not an access modifier. It answers a different question:

  • Access modifiers answer: Who may use this?
  • static answers: Does this belong to the class itself, or to each object created from the class?

A member without static is an instance member. Each object has its own instance data and uses instance methods through an object reference. A static member belongs once to the type itself.

Mental Model

Think of a class as an apartment building.

  • public is the building lobby: anyone allowed into the building can use it.
  • private is an apartment's locked safe: only that apartment can access it.
  • protected is a private family space: the apartment itself and apartments belonging to its inherited family can access it.
  • No modifier means C# applies the building's default rule for that location.

Now think of static as the difference between a shared building noticeboard and each apartment's own mailbox:

  • A static field is the one shared noticeboard for the entire building.
  • An instance field is a separate mailbox for every apartment.

Accessibility and static can be combined. For example, a field can be private static: private to the class but shared by every instance.

Take Quiz

Syntax and Examples

A class can expose a small public API while keeping implementation details private.

public class BankAccount
{
    private decimal balance;

    public string Owner { get; }

    public BankAccount(string owner, decimal openingBalance)
    {
        Owner = owner;
        balance = openingBalance;
    }

    public void Deposit(decimal amount)
    {
        if (amount <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount));
        }

        balance += amount;
    }

    public decimal GetBalance()
    {
        return balance;
    }
}

BankAccount is public, so another project can use the type if it references the assembly. Deposit and GetBalance are public because they are intended operations for callers. balance is private, so callers cannot directly set it to an invalid value.

var account = new BankAccount("Mina", 100m);
account.Deposit(25m);

Console.WriteLine(account.GetBalance()); // 125
// account.balance = -500m; // Does not compile: balance is private.

protected is useful when a base class provides data or behavior specifically for derived classes:

public class Employee
{
    protected decimal BaseSalary { get; }

    public Employee(decimal baseSalary)
    {
        BaseSalary = baseSalary;
    }
}

public class SalesEmployee : Employee
{
    public SalesEmployee(decimal baseSalary) : base(baseSalary)
    {
    }

    public decimal CalculatePay(decimal commission)
    {
        return BaseSalary + commission;
    }
}

SalesEmployee can read BaseSalary because it derives from Employee. Regular outside code cannot access BaseSalary directly.

Static and instance members can appear in the same class:

public class VisitorCounter
{
    public static int TotalVisitors { get; private set; }

    public string Name { get; }

    public VisitorCounter(string name)
    {
        Name = name;
        TotalVisitors++;
    }
}

var first = new VisitorCounter("Ava");
var second = new VisitorCounter("Noah");

Console.WriteLine(first.Name);                  // Ava
Console.WriteLine(VisitorCounter.TotalVisitors); // 2

Name belongs to each object. TotalVisitors is one value shared by the VisitorCounter type.

Step by Step Execution

Consider this code:

public class ScoreTracker
{
    private int score;
    public static int CreatedCount { get; private set; }

    public ScoreTracker()
    {
        CreatedCount++;
    }

    public void AddPoints(int points)
    {
        score += points;
    }

    public int GetScore()
    {
        return score;
    }
}

var playerOne = new ScoreTracker();
playerOne.AddPoints(10);

var playerTwo = new ScoreTracker();
playerTwo.AddPoints(5);

Console.WriteLine(playerOne.GetScore());      // 10
Console.WriteLine(playerTwo.GetScore());      // 5
Console.WriteLine(ScoreTracker.CreatedCount); // 2

Execution trace:

  1. playerOne is created. Its instance field score starts at 0.
  2. The constructor increments the shared static CreatedCount from 0 to 1.
  3. playerOne.AddPoints(10) changes only playerOne's private score to 10.
  4. playerTwo is created. It gets a separate score field, also starting at 0.
  5. The constructor increments the single shared CreatedCount from 1 to 2.
  6. playerTwo.AddPoints(5) changes only playerTwo's score to 5.
  7. The two objects have different instance scores, while both contribute to the same static count.

Outside the class, code can call GetScore because it is public. It cannot read or write score directly because it is private.

Real World Use Cases

  • Public API methods: A service exposes public Task<User> GetUserAsync(...) so controllers and other consumers can request user data.
  • Private implementation details: A class stores a private database connection helper or calculation method so callers cannot depend on internal implementation choices.
  • Protected extension points: A framework base class gives derived classes a protected method such as ValidateRequest() or a protected property needed to customize behavior.
  • Internal application code: In a multi-project solution, internal types can be shared inside one assembly without becoming part of a library's public API.
  • Static utility methods: Math.Sqrt, string.IsNullOrWhiteSpace, and helper classes often use static methods when no object-specific state is needed.
  • Static shared state: Application-wide configuration, cached read-only data, counters, or a singleton-like shared resource may be static. Shared mutable state should be used carefully because it can make tests and concurrent code harder to manage.
Take Quiz

Real Codebase Usage

Developers usually keep fields private and provide intentional public methods or properties. This protects class invariants: rules that must always remain true.

A common validation pattern is to expose a public method while keeping mutation private:

public class UserProfile
{
    public string DisplayName { get; private set; }

    public UserProfile(string displayName)
    {
        DisplayName = ValidateName(displayName);
    }

    public void ChangeDisplayName(string newName)
    {
        DisplayName = ValidateName(newName);
    }

    private static string ValidateName(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            throw new ArgumentException("A display name is required.", nameof(value));
        }

        return value.Trim();
    }
}

Here:

  • DisplayName is publicly readable but can only be changed inside UserProfile because its setter is private.
  • ValidateName is private because it is an implementation detail.
  • ValidateName is static because it does not need data from a particular UserProfile object.

Use protected sparingly in reusable base classes. A protected member becomes part of the contract that derived classes may rely on, making future changes harder. Often, a private member plus a carefully designed protected method is safer than exposing a protected field.

public abstract class Document
{
    public void Save()
    {
        Validate();
        WriteFile();
    }

    protected virtual void Validate()
    {
    }

    private void WriteFile()
    {
        // Shared file-writing implementation.
    }
}

This lets derived classes customize validation without giving them direct access to all internal state.

Common Mistakes

Making every field public

This allows callers to put an object into an invalid state.

public class Temperature
{
    public double Celsius;
}

var temperature = new Temperature();
temperature.Celsius = -1000; // Possibly invalid for the application's rules.

Prefer private fields and validate updates through a method or property.

public class Temperature
{
    private double celsius;

    public double Celsius
    {
        get => celsius;
        set
        {
            if (value < -273.15)
            {
                throw new ArgumentOutOfRangeException(nameof(value));
            }

            celsius = value;
        }
    }
}

Assuming no modifier always means private

It depends on the declaration location.

class Helper       // internal because it is a top-level type
{
    void Run()     // private because it is a class member
    {
    }
}

Remember: top-level types default to internal; class and struct members default to private.

Treating protected as public to every object

protected is for the declaring class and derived classes. It does not mean arbitrary code can access the member.

public class Parent
{
    protected int Value = 10;
}

var parent = new Parent();
// Console.WriteLine(parent.Value); // Does not compile.

Calling an instance method using the type name

public class Greeter
{
    public void SayHello()
    {
        Console.WriteLine("Hello");
    }
}

// Greeter.SayHello(); // Does not compile: SayHello is not static.

Create an object first, or make the method static only if it does not depend on object state.

var greeter = new Greeter();
greeter.SayHello();

Using static for per-object data

public class Player
{
    public static int Score;
}

Every Player would share one score. Remove static if every player needs an independent score.

Exposing protected fields

A protected field lets derived classes change internal state without validation. Prefer a private field and protected methods or properties when inheritance genuinely needs an extension point.

Comparisons

FeatureWhat it controlsWho can access or use itTypical use
publicAccessibilityAny permitted callerA type's intended API
privateAccessibilityOnly the declaring typeFields and internal helper methods
protectedAccessibilityDeclaring type and derived typesCarefully designed inheritance hooks
internalAccessibilityCode in the same assemblyProject or library implementation types
No modifier on class memberDefault accessibilityPrivate for class/struct membersOmitted modifier, though explicit private can improve clarity
No modifier on top-level typeDefault accessibilityInternal within the assemblyNon-public top-level implementation type
staticOwnership/lifetimeAccessed through the typeShared state and stateless utilities
No staticOwnership/lifetimeAccessed through an object instanceData and behavior unique to each object

static can be used with an access modifier:

private static int nextId;
public static string ApplicationName { get; set; } = "Portal";

The first member is shared but only usable inside its class. The second is shared and publicly available.

Take Quiz

Cheat Sheet

public class Product       // Available to other assemblies
{
    private decimal price; // Only this Product class

    protected string Code { get; set; } // Product and derived classes

    public string Name { get; } // Callers can read it

    internal void Refresh() { } // Same assembly only

    public static int Count { get; private set; } // One value for Product

    public void ApplyDiscount(decimal percent) { } // Called on a Product object
}
  • Use public for a deliberate API.
  • Use private by default for fields and implementation helpers.
  • Use protected only when derived classes need an intentional extension point.
  • A top-level class without a modifier is internal.
  • A class or struct member without a modifier is private.
  • static means one member belongs to the type, not one per object.
  • Non-static members need an instance:
var product = new Product();
product.ApplyDiscount(10m);
  • Static members are normally accessed by type name:
Console.WriteLine(Product.Count);
  • private static is common for shared implementation data.
  • Avoid unnecessary public fields and mutable static state.

FAQ

What is the default access modifier for a C# class?

A top-level class declared in a namespace defaults to internal. A nested class defaults to private.

What is the default access modifier for C# methods and fields?

Methods, fields, properties, and constructors declared directly in a class or struct default to private when no modifier is written.

Should I use public or private by default in C#?

Use the least access necessary. Fields are normally private. Make a member public only when other code truly needs it as part of the class's intended API.

Can code outside a class access a protected member?

No. A protected member is accessible within the declaring class and from derived classes, subject to C#'s inheritance access rules.

Is static an access modifier in C#?

No. static controls whether a member belongs to the type or to each object instance. It is independent of public, private, and protected.

When should a C# method be static?

Make it static when it does not need instance fields, instance properties, or other instance behavior. Utility and validation methods are common examples.

Can a static member be private or public?

Yes. For example, private static int counter; is shared within the class, while public static int Counter { get; } can be accessed through the type.

Why should fields usually be private?

Private fields prevent outside code from changing internal state directly. Public methods and properties can validate changes and preserve the object's rules.

Take Quiz

Mini Project

Description

Build a small library tracker that keeps each book's availability as instance state while also keeping a shared count of all books created. The project demonstrates private fields, public behavior, a protected extension point, and a static shared value.

Goal

Create book objects that can be borrowed safely and report the total number of books created.

Requirements

  1. Create a public LibraryBook class with a title supplied by its constructor.
  2. Keep the borrowed status inaccessible for direct changes from outside the class.
  3. Provide public methods to borrow and return a book.
  4. Track the total number of created books with a static public read-only property.
  5. Create a derived ReferenceBook class that uses protected behavior to prevent borrowing.
  6. Demonstrate the behavior by creating and using both book types.
Take Quiz

Keep learning

Related questions

View more