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 methodstatic 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 instanceThe 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 want | reach for |
|---|---|
| a fixed value the whole program shares | static readonly (any value) or const (a literal) |
| a value that belongs to one object | an ordinary field |
| state that outlives a request | an 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
ToUppernever replacesstring.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.
thisanywhere 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#
- class methods — instance methods, and the member-body surface a static method shares
- Passing a function by name — method groups — passing
Slug.Ofwhere aFunc<string, string>is wanted - Method overloads — how a call site picks from a set of same-named methods
- Classes — fields,
const, and what a class is - type visibility (public / internal) —
public,privateandprotected, which combine withstatic