layout: post
title: “Built-in .NET types”
description: “Ints, strings, bools, etc”
nav: fsharp-types
seriesId: “Understanding F# types”
seriesOrder: 10
In this post we’ll take a quick look at how F# handles the standard types that are built into .NET.
Literals
F# uses the same syntax for literals that C# does, with a few exceptions.
I’ll divide the built-in types into the following groups:
- miscellaneous types (
bool
,char
, etc. ) - string types
- integer types (
int
,uint
andbyte
, etc) - float types (
float
,decimal
, etc) - pointer types (
IntPtr
, etc)
The following tables list the primitive types, with their F# keywords, their suffixes if any, an example, and the corresponding .NET CLR type.
Miscellaneous types
Object | Unit | Bool | Char (Unicode) | Char (Ascii) | |
---|---|---|---|---|---|
Keyword | obj | unit | bool | char | byte |
Suffix | B | ||||
Example | let o = obj() | let u = () | true false | ‘a’ | ‘a’B |
.NET Type | Object | (no equivalent) | Boolean | Char | Byte |
Object and unit are not really .NET primitive types, but I have included them for the sake of completeness.
String types
String (Unicode) | Verbatim string (Unicode) | Triple quoted string (Unicode) | String (Ascii) | |
---|---|---|---|---|
Keyword | string | string | string | byte[] |
Suffix | ||||
Example | “first\nsecond line” | @”C:\name” | “””can “contain”” special chars””” | “aaa”B |
.NET Type | String | String | String | Byte[] |
The usual special characters can be used inside normal strings, such as \n
, \t
, \\
, etc. Quotes must be escaped with a backslash: \'
and \"
.
In verbatim strings, backslashes are ignored (good for Windows filenames and regex patterns). But quotes need to be doubled.
Triple-quoted strings are new in VS2012. They are useful because special characters do not need to be escaped at all, and so they can handle embedded quotes nicely (great for XML).
Integer types
8 bit (Signed) | 8 bit (Unsigned) | 16 bit (Signed) | 16 bit (Unsigned) | 32 bit (Signed) | 32 bit (Unsigned) | 64 bit (Signed) | 64 bit (Unsigned) | Unlimited precision | |
---|---|---|---|---|---|---|---|---|---|
Keyword | sbyte | byte | int16 | uint16 | int | uint32 | int64 | uint64 | bigint |
Suffix | y | uy | s | us | u | L | UL | I | |
Example | 99y | 99uy | 99s | 99us | 99 | 99u | 99L | 99UL | 99I |
.NET Type | SByte | Byte | Int16 | UInt16 | Int32 | UInt32 | Int64 | UInt64 | BigInteger |
BigInteger
is available in all versions of F#. From .NET 4 it is included as part of the .NET base library.
Integer types can also be written in hex and octal.
- The hex prefix is
0x
. So0xFF
is hex for 255. - The octal prefix is
0o
. So0o377
is octal for 255.
Floating point types
32 bit floating point | 64 bit (default) floating point | High precision floating point | |
---|---|---|---|
Keyword | float32, single | float, double | decimal |
Suffix | f | m | |
Example | 123.456f | 123.456 | 123.456m |
.NET Type | Single | Double | Decimal |
Note that F# natively uses float
instead of double
, but both can be used.
Pointer types
Pointer/handle (signed) | Pointer/handle (unsigned) | |
---|---|---|
Keyword | nativeint | unativeint |
Suffix | n | un |
Example | 0xFFFFFFFFn | 0xFFFFFFFFun |
.NET Type | IntPtr | UIntPtr |
Casting between built-in primitive types
Note: this section only covers casting of primitive types. For casting between classes see the series on object-oriented programming.
There is no direct “cast” syntax in F#, but there are helper functions to cast between types. These helper functions have the same name as the type (you can see them in the Microsoft.FSharp.Core
namespace).
So for example, in C# you might write:
var x = (int)1.23
var y = (double)1
In F# the equivalent would be:
let x = int 1.23
let y = float 1
In F# there are only casting functions for numeric types. In particular, there is no cast for bool, and you must use Convert
or similar.
let x = bool 1 //error
let y = System.Convert.ToBoolean(1) // ok
Boxing and unboxing
Just as in C# and other .NET languages, the primitive int and float types are value objects, not classes. Although this is normally transparent, there are certain
occasions where it can be an issue.
First, lets look at the transparent case. In the example below, we define a function that takes a parameter of type Object
, and simply returns it.
If we pass in an int
, it is silently boxed into an object, as can be seen from the test code, which returns an object
not an int
.
// create a function with parameter of type Object
let objFunction (o:obj) = o
// test: call with an integer
let result = objFunction 1
// result is
// val result : obj = 1
The fact that result
is an object, not an int, can cause type errors if you are not careful. For example, the result cannot be directly compared with the original value:
let resultIsOne = (result = 1)
// error FS0001: This expression was expected to have type obj
// but here has type int
To work with this situation, and other similar ones, you can convert a primitive type to an object directly, by using the box
keyword:
let o = box 1
// retest the comparison example above, but with boxing
let result = objFunction 1
let resultIsOne = (result = box 1) // OK
To convert an object back to an primitive type, use the unbox
keyword, but unlike box
, you must either supply a specific type to unbox to, or be sure that the compiler has enough information to make an accurate type inference.
// box an int
let o = box 1
// type known for target value
let i:int = unbox o // OK
// explicit type given in unbox
let j = unbox<int> o // OK
// type inference, so no type annotation needed
let k = 1 + unbox o // OK
So the comparison example above could also be done with unbox
. No explicit type annotation is needed because it is being compared with an int.
let result = objFunction 1
let resultIsOne = (unbox result = 1) // OK
A common problem occurs if you do not specify enough type information — you will encounter the infamous “Value restriction” error, as shown below:
let o = box 1
// no type specified
let i = unbox o // FS0030: Value restriction error
The solution is to reorder the code to help the type inference, or when all else fails, add an explicit type annotation. See the post on type inference for more tips.
Boxing in combination with type detection
Let’s say that you want to have a function that matches based on the type of the parameter, using the :?
operator:
let detectType v =
match v with
| :? int -> printfn "this is an int"
| _ -> printfn "something else"
Unfortunately, this code will fail to compile, with the following error:
// error FS0008: This runtime coercion or type test from type 'a to int
// involves an indeterminate type based on information prior to this program point.
// Runtime type tests are not allowed on some types. Further type annotations are needed.
The message tells you the problem: “runtime type tests are not allowed on some types”.
The answer is to “box” the value which forces it into a reference type, and then you can type check it:
let detectTypeBoxed v =
match box v with // used "box v"
| :? int -> printfn "this is an int"
| _ -> printfn "something else"
//test
detectTypeBoxed 1
detectTypeBoxed 3.14