2. Types, immutable variables, structs and methods
Sections
Reading settings
No voice for this language in this browser.
More voices
Code and English terms
Off: the page’s voice reads everything, with no pause between two voices.
No voice for this language in this browser.
More voices
More voices: Microsoft Edge has natural online voices, and Windows (Settings › Time & language › Speech), macOS and Android install others.
Code: examples/l02_variables.v and examples/l02_structs.v — v run examples/l02_structs.v from code/v-for-csharp-java.
Variables
Section titled “Variables”course := 'V for C#/Java developers'lessons := 4mut done := 0done = 2println('${course}: ${done}/${lessons} lessons')V for C#/Java developers: 2/4 lessons| C# | Java | V | |
|---|---|---|---|
| Declare, type inferred | var lessons = 4; |
var lessons = 4; |
lessons := 4 |
| Declare, type given | long big = 3_000_000_000; |
long big = 3_000_000_000L; |
big := i64(3_000_000_000) |
| Can’t be reassigned | const, readonly field |
final var |
the default |
| Can be reassigned | the default | the default | mut done := 0 |
| Assign | done = 2; |
done = 2; |
done = 2 |
:= is the only way to declare a variable, so a variable always has a value. = only assigns, and only to a mut variable:
fn main() { done := 0 done = 2 println(done)}e02_immutable.v:3:2: error: `done` is immutable, declare it with `mut` to make it mutable 1 | fn main() { 2 | done := 0 3 | done = 2 | ~~~~ 4 | println(done) 5 | }A variable can’t reuse the name of a variable of an enclosing block. C# (CS0136) and Java reject this too:
fn main() { total := 10 if total > 5 { total := 20 println(total) }}e02_shadowing.v:4:3: error: redefinition of `total` 2 | total := 10 3 | if total > 5 { 4 | total := 20 | ~~~~~ 5 | println(total) 6 | }An unused variable is a warning, like C#’s CS0219, where javac says nothing:
w02_unused.v:2:2: warning: unused variable: `count` 1 | fn main() { 2 | count := 3 | ~~~~~ 3 | println('done') 4 | }doneThe same file with v -prod doesn’t compile: the warnings become errors in production builds.
e02_unused_prod.v:3:2: error: unused variable: `count` 1 | // flags: -prod 2 | fn main() { 3 | count := 3 | ~~~~~ 4 | println('done') 5 | }Primitive types
Section titled “Primitive types”| V | C# | Java | Notes |
|---|---|---|---|
bool |
bool |
boolean |
|
i8, i16, int, i64 |
sbyte, short, int, long |
byte, short, int, long |
int is always 32 bits |
u8, u16, u32, u64 |
byte, ushort, uint, ulong |
— | Java has no unsigned types |
f32, f64 |
float, double |
float, double |
|
isize, usize |
nint, nuint |
— | the size of a pointer |
rune |
int holding a code point, Rune |
int holding a code point |
a Unicode code point; char in C# and Java is a UTF-16 unit |
string |
string (UTF-16) |
String (UTF-16) |
UTF-8, immutable |
Source: V types. An integer literal is an int, a float literal an f64, and a conversion is written like a call: i64(3_000_000_000).
big := i64(3_000_000_000)println('${typeof(lessons).name} ${typeof(big).name}')total := big + lessonsprintln('${typeof(total).name} ${total}')int i64i64 3000000004V promotes a smaller type to a larger one when no value can be lost: int to i64 or f64, u8 to int. It never mixes signed and unsigned, where C# would silently convert uint + int to long:
fn main() { offset := -1 size := u32(5) println(size + offset)}e02_mixed_signs.v:4:10: error: mismatched types `u32` and `int` 2 | offset := -1 3 | size := u32(5) 4 | println(size + offset) | ~~~~~~~~~~~~~ 5 | }e02_mixed_signs.v:4:2: error: `println` can not print void expressions 2 | offset := -1 3 | size := u32(5) 4 | println(size + offset) | ~~~~~~~~~~~~~~~~~~~~~~ 5 | }The second error is a consequence of the first: once the addition has no type, println has nothing to print. Read the first error first.
Nor does V convert a number to a string. "lessons: " + 4 is valid C# and Java; in V, use interpolation:
fn main() { lessons := 4 println('lessons: ' + lessons)}e02_string_plus_int.v:3:10: error: infix expr: cannot use `int` (right expression) as `string` 1 | fn main() { 2 | lessons := 4 3 | println('lessons: ' + lessons) | ~~~~~~~~~~~~~~~~~~~~~ 4 | }Arithmetic behaves like C# in an unchecked context and like Java: integers wrap around, and integer division truncates toward zero.
max := 2147483647println(max + 1)println(i8(127) + 1)println('${7 / 2} ${-7 / 2} ${-7 % 2}')-2147483648-1283 -3 -1Strings are UTF-8 bytes
Section titled “Strings are UTF-8 bytes”word := 'journée'println('${word.len} bytes, ${word.runes().len} runes')println('${word[0]} ${typeof(word[0]).name} ${`é`} ${typeof(`é`).name}')8 bytes, 7 runes106 u8 é runeIn C# and Java, "journée".Length and "journée".length() are 7: they count UTF-16 units. In V, len counts the bytes of the UTF-8 encoding, and é takes two. Indexing a string gives a byte (u8), not a character: word[0] is 106, the code of j. runes() decodes the code points; a rune literal uses backquotes.
if and match are expressions
Section titled “if and match are expressions”V has no ternary operator: if returns a value. match is the equivalent of a C# switch expression or a Java switch with arrows, and must cover every case, here with else:
status := if done == lessons { 'finished' } else { 'in progress' }label := match done { 0 { 'not started' } 1, 2, 3 { 'started' } else { 'done' }}in progressstartedStructs instead of classes
Section titled “Structs instead of classes”V has no classes, no inheritance and no constructors. A struct holds the data, and methods are declared next to it:
struct Page { url string locale string = 'en' title string lines int}mission := Page{ url: '/v-for-csharp-java/' title: 'Mission' lines: 80}lesson := Page{'/v-for-csharp-java/02-types-structs-methods/', 'en', '2. Types, structs and methods', 300}- A struct literal names the fields, or gives them all in order.
- A field that isn’t given takes its default value:
localeis'en', and the zero value otherwise (0,'', an empty array). - There is no
null: astringfield is never missing, only empty.
@[required] makes a field mandatory in every literal, like the required modifier in C# 11:
struct Page { url string @[required] title string}
fn main() { p := Page{ title: 'Mission' } println(p)}e02_required_field.v:7:7: error: field `Page.url` must be initialized 5 | 6 | fn main() { 7 | p := Page{ | ~~~~~ 8 | title: 'Mission' 9 | }Fields are private and immutable by default
Section titled “Fields are private and immutable by default”struct Page { title string lines int}
fn main() { mut p := Page{'Mission', 80} p.lines = 81 println(p)}e02_immutable_field.v:8:4: error: field `lines` of struct `Page` is immutable 6 | fn main() { 7 | mut p := Page{'Mission', 80} 8 | p.lines = 81 | ~~~~~ 9 | println(p) 10 | }mut p makes the variable mutable, not its fields. Access modifiers are sections of the struct:
| V section | Read from another module | Write in the module | Write from another module | Closest C# | Closest Java |
|---|---|---|---|---|---|
| (none) | no | no | no | private readonly |
private final |
mut: |
no | yes | no | private |
private |
pub: |
yes | no | no | public + init |
public final |
pub mut: |
yes | yes | no | public + private set |
public getter, private setter |
__global: |
yes | yes | yes | public |
public |
The course code uses one module, so it only needs mut::
struct Progress {mut: done intpub: total int}Methods
Section titled “Methods”fn (p Page) is_lesson() bool { return p.title.len > 0 && p.title[0].is_digit()}
fn (mut p Progress) complete() { p.done++}A method is a function with a receiver between fn and its name. There is no this: the receiver has a name, by convention one letter. The receiver is immutable unless declared mut, and a mut method can only be called on a mutable variable:
fn main() { p := Progress{} p.complete() println(p.done)}e02_mut_receiver.v:12:2: error: `p` is immutable, declare it with `mut` to make it mutable 10 | fn main() { 11 | p := Progress{} 12 | p.complete() | ^ 13 | println(p.done) 14 | }The same rule applies to parameters: fn f(mut pages []Page) is called as f(mut pages), so the call site shows what can change. But V refuses mut for a number, where C# has ref int:
fn double(mut n int) { n *= 2}e02_mut_int_param.v:1:17: error: mutable arguments are only allowed for arrays, interfaces, maps, pointers, structs or their aliasesreturn values instead: `fn foo(mut n int) {` => `fn foo(n int) int {` 1 | fn double(mut n int) { | ~~~ 2 | n *= 2 3 | }str(), static methods and struct update
Section titled “str(), static methods and struct update”fn (p Page) str() string { return '${p.title} (${p.locale}, ${p.lines} lines)'}
fn Page.translation(p Page, locale string, title string) Page { return Page{ ...p url: '/${locale}${p.url}' locale: locale title: title }}str()plays the role ofToString()andtoString():printlnand${…}call it.fn Page.translationis a static method, calledPage.translation(…). It is the V way to write a factory, since there are no constructors....pcopies the fields ofpand replaces the ones listed, like a C#withexpression on a record.
println(mission)println(lesson.is_lesson())french := Page.translation(lesson, 'fr', '2. Types, structs et méthodes')println(french.url)Mission (en, 80 lines)true/fr/v-for-csharp-java/02-types-structs-methods/Named and optional arguments
Section titled “Named and optional arguments”V has no overloading, no default arguments and no named arguments. Two functions with the same name are rejected, whatever their parameters:
fn describe(lines int) string { return '${lines} lines'}
fn describe(title string) string { return 'title: ${title}'}builder error: redefinition of function `describe`e02_overload.v:1:1: conflicting declaration: fn describe(lines int) string 1 | fn describe(lines int) string { | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | return '${lines} lines' 3 | }e02_overload.v:5:1: conflicting declaration: fn describe(title string) string 3 | } 4 | 5 | fn describe(title string) string { | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 6 | return 'title: ${title}' 7 | }A struct marked @[params], as the last parameter, gives the same comfort as named and optional arguments in C#, or a builder in Java:
@[params]struct ListOptions { only_lessons bool max int = 10}
fn list(pages []Page, opts ListOptions) { // …}
list(pages, max: 2)list(pages, only_lessons: true)list(pages)The call lists the fields without braces; the ones left out keep their default value.
Structs are values
Section titled “Structs are values”copy := lessonprintln(copy == lesson)mut p1 := Progress{ total: 4}mut p2 := p1p2.complete()p1.complete()p1.complete()println('p1 ${p1.done}/${p1.total}, p2 ${p2.done}/${p2.total}')println(p1)truep1 2/4, p2 1/4Progress{ done: 2 total: 4}A V struct behaves like a C# struct or record struct, not like a class: assignment copies it, and == compares the fields, like the equality of a C# record or a Java record. mut p2 := p1 makes a second Progress. A struct without str() prints all its fields.
Behind the scenes, V passes an immutable struct argument by value or by reference, as it sees fit (references); since the callee can’t change it, the difference doesn’t show. Lesson 4 comes back to references, &Page, and to what is shared: arrays and maps don’t follow the same rule as structs.
Key takeaways
Section titled “Key takeaways”x := 1declares an immutable variable;mutis needed to reassign it, to change a field, or to call amutmethod.intis 32 bits everywhere; V never mixes signed and unsigned types, and never converts a number to a string implicitly.- A string is UTF-8:
lencounts bytes, indexing gives au8,runes()gives the code points. - A struct has fields that are private and immutable by default, methods with an explicit receiver, and no constructor: static methods and
...preplace them. - No overloading, no default or named arguments: an
@[params]struct replaces them. - Structs are values, copied on assignment and compared field by field.
Exercises
Section titled “Exercises”- Write a struct
Coursewith a name, a number of lessons and a mutable counterdone, a methodfinish_lesson()that never goes past the number of lessons, and astr()that printsV: 2/2. Callfinish_lesson()three times on a course of two lessons.
Solution
struct Course { name string lessons intmut: done int}
fn (mut c Course) finish_lesson() { if c.done < c.lessons { c.done++ }}
fn (c Course) str() string { return '${c.name}: ${c.done}/${c.lessons}'}
fn main() { mut v := Course{ name: 'V' lessons: 2 } for _ in 0 .. 3 { v.finish_lesson() } println(v)}V: 2/2for _ in 0 .. 3 repeats three times; _ ignores the counter, and 0 .. 3 excludes 3.
- What does this print? Would a C#
class Courseprint the same?
mut a := Course{ name: 'Rust' lessons: 15}mut b := ab.finish_lesson()println('${a} | ${b}')Solution
Rust: 0/15 | Rust: 1/15mut b := a copies the struct: a doesn’t change. With a C# class, b and a would be the same object and both would print Rust: 1/15; with a C# struct, the output would be the same as in V.
fn double(mut n int)doesn’t compile. Write the function V expects instead, and call it.
Solution
Return the new value, as the error message suggests:
fn double(n int) int { return n * 2}
x := double(2)V allows mut parameters only for arrays, maps, structs, interfaces and pointers. The documentation even prefers returning values for those, and keeps changing an argument in place for the parts of a program where copies cost too much.