Osy#betaRun it on Osyrin: osyrin.com

Reference / Class

`static` methods, `static readonly`, `static class` and extension methods

static <Return> <Name>(<params>) { … } public static readonly <Type> <Name> = <value>; static class <Name> { … } public static <Return> <Name>(this <Type> value, <params>) { … }

A `static` member belongs to the type rather than to any instance and is reached on the type name — `Money.Round(x)`, `Limits.Max`. A static method has no `this`. An IMMUTABLE static (`static readonly`, a getter-only static property) holds a fixed value; a static that can be assigned is refused. A `static class` holds only static members, and is where C# extension methods live — `this string s` on the first parameter makes `"hi".Shout()` work.

stable11 examples compiled by CIclassmethodsstatic

Summary#

A static method is called on the type, not on a value:

class Money {
  public decimal Amount;

  public static decimal Round(decimal value) {
    return Math.Round(value, 2);
  }
}

decimal RoundAPrice() {
  return Money.Round(19.999m);       // on the TYPE — there is no Money instance here
}

Reach for it when the operation is about the type but not about any particular value of it — a conversion, a validation, a calculation over its arguments.

Signature#

static <Return> <Name>(<params>) { … }
public static <Return> <Name>(<params>) { … }
public static readonly <Type> <Name> = <value>;
public static <Type> <Name> => <value>;
static class <Name> { <static members> }
public static <Return> <Name>(this <Type> value, <params>) { … }     // in a static class: an extension method

static goes after the visibility word, as in C#. On a method it means "no instance"; on a field or property it is allowed only when the value cannot be assigned (readonly, or a getter with no setter).

Description#

A static method has no this#

That is the whole of the rule, and everything else follows from it. There is no instance, so there is nothing for an instance field to be read from:

class Money {
  public decimal Amount;

  public static decimal Doubled() {
    return Amount * 2m;              // ✗ 'Amount' belongs to an INSTANCE
  }

  public static decimal Half() {
    return this.Amount / 2m;         // ✗ `this` has no meaning in a static method
  }
}

Take the value as a parameter instead — which is usually what the method wanted anyway:

class Tax {
  public static decimal Net(decimal gross, decimal rate) {
    return gross / (1m + rate);
  }
}

What a static method CAN see of its own class#

Its other static methods and its const values, both by bare name — the same rule as C#:

class Rate {
  public const decimal Standard = 0.25m;

  public static decimal Apply(decimal amount) {
    return amount * (1m + Standard);      // a const, by bare name
  }

  public static decimal ApplyTwice(decimal amount) {
    return Apply(Apply(amount));          // another static, by bare name
  }
}

[Test]
void Statics_Reach_Statics_And_Consts() {
  Assert.Equal(125m, Rate.Apply(100m));
  Assert.Equal(156.25m, Rate.ApplyTwice(100m));
}

An instance method may call its class's static methods the same way — it simply does not pass its this along:

class Line {
  public decimal Gross;

  public decimal Net() {
    return Tax.Net(Gross, 0.25m);
  }
}

Call it on the type, never through a value#

The two directions are both compile errors, and each says which spelling to use:

var m = new Money { Amount = 1m };

m.Round(2.5m);          // ✗ 'Money.Round' is static — call it as `Money.Round(…)`
Money.Amount;           // ✗ 'Money.Amount' is an instance member — access it through an instance

The type may be written in full wherever its short name may: Acme.Billing.Money.Round(2.5m) and Acme.Billing.Money.Zero are the same members, reached from a file that does not import Acme.Billing. A member the class does not have is refused by the class's name: the class 'Acme.Billing.Money' has no static member 'Nope'.

Refusing the first is C#'s rule too, and it is worth the strictness: m.Round(…) reads as though the method can see m, and it cannot — the receiver would be evaluated and discarded.

Statics inherit#

A static method declared on a base class is callable on a derived one, like any other inherited member:

class Shape {
  public string Name;
  public static decimal Zero() { return 0m; }
}

class Circle : Shape {
  public decimal Radius;
}

decimal ZeroThroughTheSubclass() {
  return Circle.Zero();          // Shape declares it; Circle inherits it
}

It cannot be virtual, override or abstract, and combining them is a compile error. Those words choose a body from the receiver's runtime type, and a static call has no receiver to choose by.

Static methods overload#

Exactly like instance methods — a name maps to a set, and the call site picks by the arguments. See Method overloads for the rule.

class Fmt {
  public static string Of(decimal d) { return d.ToString(); }
  public static string Of(decimal d, string unit) { return d.ToString() + " " + unit; }
}

A static field must be immutable#

A static that can be ASSIGNED is refused, in as many words. One host process serves many applications, so a mutable static would be state shared by all of them, with no per-app copy to reset:

class Counter {
  public static decimal Total;         // ✗ cannot be a MUTABLE `static`
}

An IMMUTABLE one is fine, because there is nothing in it anyone can change — see static readonly below. For the rest:

what you wantreach for
a fixed value the whole program sharesstatic readonly (any value) or const (a literal)
a value that belongs to one objectan ordinary field
state that outlives a requestan entity — stored, per-app, and secured

For the same reason there is no static constructor: each static readonly is given its value where it is declared.

A fixed value on the type: static readonly#

static readonly and a getter-only static property are read on the type name, or by bare name inside the class — C#'s rule exactly:

class Shipping {
  public static readonly decimal FreeFrom = 500m;
  public static int MaxParcels => 3;
  public static readonly string[] Carriers = ["PostNord", "DHL"];

  public static bool IsFree(decimal total) => total >= FreeFrom;   // bare name, inside the class
}

[Test]
void Immutable_Statics_Read_Like_Fields() {
  Assert.Equal(500m, Shipping.FreeFrom);
  Assert.Equal(3, Shipping.MaxParcels);
  Assert.Equal(2, Shipping.Carriers.Length);
  Assert.True(Shipping.IsFree(600m));
}

When does the initializer run, and how often?#

ONCE — the first time the field is read — and the value is then kept, as C#'s static initializer keeps it: once per running version of the app on each server, and once per page load in the browser. So Guid.NewGuid() is one id, and a list the field holds keeps what is added to it. A getter-only static PROPERTY (static int Max => …;) is the other C# shape and runs on every read:

static class Boot {
  public static readonly Guid Started = Guid.NewGuid();
  public static readonly List<string> Seen = new List<string>();
  public static Guid Fresh => Guid.NewGuid();
}

[Test]
void A_Static_Readonly_Is_Kept() {
  Assert.Equal(Boot.Started, Boot.Started);
  Boot.Seen.Add("a");
  Assert.Equal(1, Boot.Seen.Count);
  Assert.True(Boot.Fresh != Boot.Fresh);
}

⚠ Because it is shared by every request the server handles, the initializer may not read stored data, who is signed in, or do work with an effect — the first caller's answer would be every later caller's. That is a compile error that says so; read such a value in a function, or as a static property, which runs per read. And a static readonly cannot be assigned after it is declared (C#'s rule); what it holds can still be changed through it.

A holder of helpers: static class#

A static class holds only static members — C# refuses an instance member in one (CS0708), and so does Osy#, naming the member. It is never created (new Text() is refused) and nothing derives from it:

static class Slug {
  public static readonly string Separator = "-";
  public static string Of(string title) => title.Trim().ToLower().Replace(" ", Separator);
}

string SlugFor(string title) => Slug.Of(title);

It takes a visibility like any class, written before static as in C#:

public static class Measures {
  public const int Dozen = 12;
  public static int Dozens(int n) => n * Dozen;
}

int Eggs() => Measures.Dozens(3);

Adding a verb to a type you do not own: extension methods#

this on the FIRST parameter of a static method in a static class makes an extension method: the value it is called on is passed as that parameter. "Hello".Shout() is Text.Shout("Hello"):

static class Text {
  public static string Shout(this string s) => s.ToUpper() + "!";
  public static string Wrap(this string s, string with) => with + s + with;
}

static class Numbers {
  public static bool IsEven(this int n) => n % 2 == 0;
}

class Order { public int Id; }

static class OrderLinks {
  public static string Href(this Order o) => $"/orders/{o.Id}";
}

[Test]
void Extension_Methods_Read_Like_Members() {
  Assert.Equal("HELLO!", "Hello".Shout());
  Assert.Equal("*a*", "a".Wrap("*"));
  Assert.True(4.IsEven());
  var orders = new List<Order> { new Order { Id = 1 }, new Order { Id = 2 } };
  Assert.Equal("/orders/1,/orders/2", string.Join(",", orders.Select(o => o.Href())));
}

The rules are C#'s:

  • An instance member wins. An extension is tried only when nothing on the type itself applies — an extension named ToUpper never replaces string.ToUpper().
  • Two that both apply are an error, naming both — call the one you mean on its class (Text.Shout(s)).
  • It is in scope wherever its class is. An extension in a kit is available to every app that uses the kit.
  • this anywhere else — on a method outside a static class, or on a parameter that is not the first — is refused, saying where it belongs.

See also#

Related

class methods

Behaviour attached to a class — a method with a receiver, called as value.Method(). Classes are in-memory values, so a…

Classes

A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is…

Method overloads

A class can declare several methods with the same name, as long as they differ in their parameter types. Each call…

constructor

A class declares one constructor — its name is the class name, it takes no return type, and it runs when you write new…

type visibility (public / internal)

A top-level declaration carries a public or internal visibility that decides whether code outside its package can name…