Variables and Data Types in C#: var, const and Literals
Statically typed variables in C#, the sizes and ranges of the built-in types, what var really does, literal suffixes and the compiler errors around them, const and C# 10's constant interpolated strings, default values and .NET naming conventions.
Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.
Guess First
Six variables, all declared with var. The values on the right look very similar; some differ by a single letter at the end. The program prints the name of each variable's type. Guess the two lines of output:
var a = 42;
var b = 42L;
var c = 4.2;
var d = 4.2m;
var e = 'x';
var f = "x";
Console.WriteLine($"{a.GetType().Name} {b.GetType().Name} {c.GetType().Name}");
Console.WriteLine($"{d.GetType().Name} {e.GetType().Name} {f.GetType().Name}");
Int32 Int64 Double
Decimal Char String
Six different types. 42 is an int; add an L and it becomes a long. 4.2 is a double; add an m and it's a decimal. Single quotes make a character, double quotes a string. var didn't "choose" any type here; the value on the right decided it. In this post we look at where a variable's type comes from in C#, the built-in types, literals, const and naming rules.
A Variable: A Labeled Box
A variable is a named place in memory that holds a value. Picture a box: it has a label (the name), and only items of a certain shape (the type) fit inside. In C#, when you declare a variable you write the type first, then the name, and optionally the initial value on the same line:
int itemsInCart = 3;
itemsInCart = itemsInCart + 2;
C# is a statically typed language: a variable's type is known at compile time and never changes. If you try to put an item of a different shape into the box, the compiler stops you. Write var stock = 10; and then stock = "ten"; on the next line, and you get:
Program.cs(1,25): error CS0029: Cannot implicitly convert type 'string' to 'int'
If you come from JavaScript or Python, this may feel restrictive at first. But the same restriction means that in a big project you always know the answer to "what's in this variable?" You see the mistake on your own screen at compile time, not on a customer's screen while the app is running.
Another rule: you can't read a local variable before giving it a value. Write int stock; followed by Console.WriteLine(stock); and the compiler says error CS0165: Use of unassigned local variable 'stock'. The compiler wants to be sure the variable is assigned on every path before it's read.
Built-In Types
C#'s built-in types written as keywords are, as we saw in the earlier posts, short names for .NET types: int is really System.Int32, string is really System.String. We asked the program itself how much memory the numeric types take and what range of values they can hold:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
Console.WriteLine($"byte {sizeof(byte),2} bytes {byte.MinValue} .. {byte.MaxValue}");
Console.WriteLine($"short {sizeof(short),2} bytes {short.MinValue} .. {short.MaxValue}");
Console.WriteLine($"int {sizeof(int),2} bytes {int.MinValue} .. {int.MaxValue}");
Console.WriteLine($"long {sizeof(long),2} bytes {long.MinValue} .. {long.MaxValue}");
Console.WriteLine($"float {sizeof(float),2} bytes {float.MinValue} .. {float.MaxValue}");
Console.WriteLine($"double {sizeof(double),2} bytes {double.MinValue} .. {double.MaxValue}");
Console.WriteLine($"decimal {sizeof(decimal),2} bytes {decimal.MinValue} .. {decimal.MaxValue}");
Console.WriteLine($"char {sizeof(char),2} bytes {(int)char.MinValue} .. {(int)char.MaxValue}");
Console.WriteLine($"bool {sizeof(bool),2} bytes {false} / {true}");
byte 1 bytes 0 .. 255
short 2 bytes -32768 .. 32767
int 4 bytes -2147483648 .. 2147483647
long 8 bytes -9223372036854775808 .. 9223372036854775807
float 4 bytes -3.4028235E+38 .. 3.4028235E+38
double 8 bytes -1.7976931348623157E+308 .. 1.7976931348623157E+308
decimal 16 bytes -79228162514264337593543950335 .. 79228162514264337593543950335
char 2 bytes 0 .. 65535
bool 1 bytes False / True
The first two lines make the way numbers are printed independent of culture; on a computer set to Turkish, for example, the decimal separator is a comma and the output looks different. We'll open up the topic of culture in the posts on strings and type conversions.
For most everyday work these types are enough: int for whole numbers, long for numbers too big for an int, double for fractional measurements, decimal for money, bool for yes/no, string for text. char holds a single UTF-16 character, which is why it's 2 bytes. There are unsigned siblings too: sbyte, ushort, uint, ulong; but you rarely need them in application code. We'll look at the differences between the numeric types, especially between double and decimal, in detail in the next post.
With these types we can describe a book in our bookstore like this:
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
string title = "Clean Code";
string isbn = "978-0132350884";
int pageCount = 464;
decimal price = 34.90m;
double rating = 4.7;
bool inStock = true;
char shelf = 'C';
long totalSold = 1_250_000L;
Console.WriteLine($"{title} ({isbn})");
Console.WriteLine($"{pageCount} pages, {price} USD, rating {rating}");
Console.WriteLine($"In stock: {inStock}, shelf {shelf}, sold {totalSold}");
Clean Code (978-0132350884)
464 pages, 34.90 USD, rating 4.7
In stock: True, shelf C, sold 1250000
Notice that the ISBN is a string. It contains digits, but you won't do math with it; it contains dashes, and leading zeros must not get lost. Phone numbers, postal codes and national ID numbers are stored as text rather than numbers for the same reason.
var: The Compiler Finds the Type
var means "infer this variable's type from the value on the right". As you saw in Guess First, there's no difference at all in the compiled code between var a = 42; and int a = 42;. var isn't a dynamic type; the type is still decided at compile time and stays fixed. That's why var has two conditions: you must give the variable a value on the line where you declare it, and that value must have a type.
Program.cs(1,5): error CS0818: Implicitly-typed variables must be initialized
Program.cs(1,5): error CS0815: Cannot assign <null> to an implicitly-typed variable
The first comes from var stock;, the second from var title = null;. Since null has no type of its own, the compiler can't know what you mean.
When should you use var? The general rule: if the type is obvious from the right-hand side, var improves readability; if it isn't, write the explicit type. In var builder = new StringBuilder(); the type is already written on the right. In var total = Calculate();, though, the reader can't know the type without going to the method; decimal total = Calculate(); is much clearer. While you're starting out, I recommend making explicit types a habit: thinking about which value has which type is the real payoff of the first posts in this series.
Literals
Values you write directly in the code are called literals: 42, 4.2, 'x', "Clean Code", true. Every literal has a type, and the letter you add at the end changes that type:
int plain = 255;
int hex = 0xFF;
int binary = 0b1111_1111;
long population = 85_000_000L;
uint positiveOnly = 42u;
float ratio = 0.75f;
double average = 0.75;
decimal discount = 0.75m;
char letter = 'A';
char tab = '\t';
Console.WriteLine(plain == hex && hex == binary);
Console.WriteLine($"{population} {positiveOnly} {letter}{tab}{letter}");
Console.WriteLine($"{ratio.GetType().Name} {average.GetType().Name} {discount.GetType().Name}");
True
85000000 42 A A
Single Double Decimal
You can write the same number in decimal, hexadecimal (0x) and binary (0b); all three are 255. The underscore is only for readability and the compiler ignores it: 85_000_000 and 85000000 are the same thing. Also notice that the .NET name of float is Single.
When you forget a suffix, the compiler doesn't just complain; it tells you what to do. When you write decimal price = 34.90;:
Program.cs(1,17): error CS0664: Literal of type double cannot be implicitly converted to type 'decimal'; use an 'M' suffix to create a literal of this type
The reason: 34.90 is a double literal, and there's no silent conversion from double to decimal. The same applies to float, where the suffix it asks for is F. For whole numbers, the literal's type is chosen by its size: 3_000_000_000, which doesn't fit in an int, becomes a uint, and the line int big = 3_000_000_000; gives error CS0266: Cannot implicitly convert type 'uint' to 'int'. An out-of-range value doesn't compile either: byte b = 256; gives error CS0031: Constant value '256' cannot be converted to a 'byte'.
const: Values That Never Change
For something whose value never changes during the program, you use const. Like the maximum number of items a cart can hold, or the store's name:
const int MaxCartItems = 20;
const string StoreName = "Bookstore";
const string Greeting = $"Welcome to {StoreName}";
int itemsInCart = 3;
itemsInCart = itemsInCart + 2;
Console.WriteLine(Greeting);
Console.WriteLine($"{itemsInCart} of {MaxCartItems} items");
Welcome to Bookstore
5 of 20 items
A const value must be known at compile time, because the compiler writes it directly into every place it's used. The Greeting line is a convenience that came with C# 10: an interpolated string whose parts are all constant strings can now be a const. But const int Length = Title.Length; doesn't compile, because Length is a property calculated at run time: error CS0133: The expression being assigned to 'Length' must be constant. If you try to assign to a const later, you get error CS0131: The left-hand side of an assignment must be a variable, property or indexer.
When to use const? If the same number appears in more than one place in the code and it has a meaning, give it a name. When you write if (itemsInCart > MaxCartItems) instead of if (itemsInCart > 20), the code explains itself, and when the limit changes you update a single line.
Default Values
We saw that you can't read local variables without assigning them. But places like array elements and class fields automatically start with the default value of their type. 0 for numbers, false for bool, '\0' for char, and null for reference types such as string:
int[] stocks = new int[3];
bool[] flags = new bool[2];
string[] titles = new string[2];
Console.WriteLine(stocks[0]);
Console.WriteLine(flags[1]);
Console.WriteLine(titles[0] is null);
Console.WriteLine(default(decimal));
Console.WriteLine(default(char) == '\0');
0
False
True
0
True
The default(T) expression gives you the default value of any type. We'll cover arrays and null in detail in their own posts. For now, know this: when you write string title = default; in a project with nullable enabled, the compiler gives the warning warning CS8600: Converting null literal or possible null value to non-nullable type. That warning means "you said this string would never be null, but you gave it null".
Naming Rules
A name starts with a letter or an underscore; it can't start with a digit. You can't use keywords as names: string class = "fiction"; produces a string of syntax errors. If you really need to, you put @ in front (@class), but that's usually only needed in code that talks to another language. C# is case-sensitive: declare pageCount and use PageCount, and you get error CS0103: The name 'PageCount' does not exist in the current context.
Rules aside, the .NET world has established habits. Local variables and parameters are camelCase: pageCount, itemsInCart. Type names, methods, properties and consts are PascalCase: MaxCartItems, StoreName. For private fields the common convention is an underscore plus camelCase: _stock. Names should carry meaning rather than abbreviations: price, not p; count, not cnt. Code is written once and read dozens of times.
Common Pitfalls
Using double for money. double price = 34.90; compiles and seems to work at first glance. The trouble shows up with sums and rounding; we'll see it with numbers in the next post. For prices, amounts and balances, use decimal and the m suffix.
Thinking var is a dynamic type. You can't later assign a value of another type to a variable declared with var. Writing var doesn't remove the type; it just leaves writing it to the compiler.
Making everything that looks like a number a number. You don't do arithmetic with ISBNs, phone numbers or postal codes. Make them int or long and you lose leading zeros and dashes.
Magic numbers. Numbers like 20, 0.18 or 365 buried in the middle of the code turn, after a while, into values whose meaning nobody remembers. Give a meaningful number a name with const.
A Small Experiment
★☆☆ Add the lines var g = 42u;, var h = 4.2f; and var i = true; to the Guess First program, guess their types and run it to check.
★★☆ Add a variable each to the book example for the author's name, the publication year and whether it's on sale. For each one, write a comment saying which type you chose and why.
★★★ Change the line long totalSold = 1_250_000L; to int and set the value to 3_000_000_000. Which error code do you get? Then go back to long but remove the L suffix; does it compile, and why?
We've met variables and types. In the next post we actually compute with numbers: the differences between int, long, double and decimal, the surprises of integer division, overflow and checked, the Math class, and why 0.1 + 0.2 doesn't equal 0.3.