C# from F# - using C# and the .NET APIs from F#

Assemblies written in C# and other .NET languages can be directly accessed from F#. This makes F# an amazingly powerful language given the huge number of libraries available. This section describes how various to use various .NET constructs from F#. A working knowledge of C# or another .NET language is assumed.

Longer examples of interoperability can be found in the samples that come with the F# compiler.

Types.  Types are accessed using the "Namespace.Type" notation.  You may simply use "Type" if an "open Namespace" declaration has been given.

Constructors. Instances of .NET types are constructed using the expression form new Type(arg1,...,argN). See also construction expressions for instances which are extensions and/or implementations as described further below.

Instance Methods. Instance members are accessed using "obj.Method(arg1,...,argN)" or "obj.Property" or "obj.Field"

Instance "Getter" Properties. Setter properties are accessed using the syntax obj.Property.

Instance "Setter" Properties. Setter properties are accessed using the syntax obj.Property <- expr.

Static Methods.  Static members are accessed using "Namespace.Type.Method(arg1,...,argN)" or "Type.Method(arg1,...,argN)".

Static "Getter" Properties. Static setter properties are accessed using the syntax Type.Property.

Static "Setter" Properties. Static setter properties are accessed using the syntax Type.Property <- expr.

Fields. Instance and static fields are accessed using the same synax as properties.

For example, to access the type System.IO.FileStream use:

let x : System.IO.FileStream = System.IO.FileStream.Open("myfile")
or 
open System.IO
  let x : FileStream = FileStream.Open("myfile")

Events.  Access these using the Add/Remove/Fire methods for the event.

Void.  Methods returning type "void" returning type "unit"

For example,

 System.Console.WriteLine()

Delegates.  Delegates values can be constructed by supplying an argument of the corresponding function type. 

For example, if the delegate signature is

   delegate void EventHandler(object,EventArgs)

then pass a function of type obj -> EventArgs -> unit, e.g.

new EventHandler(fun (x:obj) (e:EventArgs) -> ...)

or just.

new EventHandler(fun x e -> ...)

and the type checker will enforce the constraints.

F# values that implement .NET interfaces can be generated using object expressions.

Sometimes extra annotations are needed to get the program to typecheck, e.g. downcasts using "(<expr> :> <type>)" and some extra type annotations may be required to help resolve overloading. This works by a process of strict left-to-right, outer-to-inner type checking, i.e. if extra type information is needed you should add it either to the expression where the information is required itself or one a related expression to the left of the expression.

All F# and .NET values are convertible to the 'object' type - in F# this is called obj. Thus F# values can be stored in heterogeneous collections such as System.Collections.ArrayList. Conversions in ther reverse direction are dynamically checked and may raise failures. F# supports the following operators to convert to and from the 'object' type:

     box: 'a -> obj
     unbox: obj -> 'a

All .NET and F# object types are convertible to their 'base' types. Other F# types (records, discriminated unions and abstract types) are not arranged in a hierarchy and the only 'base' type of most F# types is obj. .NET values can also be re-converted to sub types, with possible runtime exceptions. F# supports the following operators to convert .NET values to and from their base types:

     expr == 
      | e :? ty      -- dynamically test if 'e' has type 'ty'.  A compile-time error
                        will occur if local type inference does not
                        infer types such that this is a valid downward type test. 
 
      | e :> ty      -- statically upcast 'e' to type 'ty'.  A compile-time error
                        will occur if local type inference does not
                        infer types such that this is a valid upcast. 
 
      | e :?> ty     -- dynamically downcast 'e' to type 'ty'.  A compile-time error
                        will occur if local type inference does not
                        infer types such that this is a valid downcast. 
 
      | downcast e   -- runtime checked downcast from 'e' to an arbitrary type 
                        inferred from the context.  A compile-time error
                        will occur if local type inference does not
                        infer types such that this is a valid downcast. 
 
      | upcast e     -- statically checked upcast from 'e' to an arbitrary type
                        inferred from the context.  A compile-time error
                        will occur if local type inference does not
                        infer types such that this is a valid upcast. 
  
    pat ==           
      | :? ty        -- the pattern corresponds to a .NET type test.  A compile-time error
                        will occur if local type inference does not
                        infer types such that this is a valid downward type test. 

      | :? ty as id  -- the pattern corresponds to a .NET type test, and if 
                        successful the variable 'id' is bound to the value at the given
                        type.

The expression "e :? ty" is equivalent to a dynamic type test. A warning will be emitted if the type of e cannot be statically determined to be a subtype of ty (statically determined == using the inference information available at this point in the typechecking of the program, where inference proceeds left-to-right through the program). An error will be reported if the type test will always succeed.

This is especially useful for testing for classes of .NET exceptions, e.g.

      try  
          ... 
      with
       | e when (e :? System.Net.Sockets.SocketException) -> ...
       | e when (e :? System.OutOfMemory) -> ...

Valid casts are those between .NET types related by class extension or interface inheritance, and also between F# reference types and the type 'obj' (i.e. System.Object).

In F# code, .NET reference types can be null values. Nulls can be checked and created using the following contructs:

     expr == 
      | e1 ?? e2     -- dynamically test if 'e1' is null, and if so evaluate e2.
                        A compile-time error will occur if local type inference
                        does not infer types such that e1 is a .NET type.  Equivalent to
                           (match e1 with null -> e2 | freshv -> freshv)
 
      | null         -- generate a null value of an arbitrary type inferred
                        from the surrounding context.  A compile-time error
                        will occur if local type inference does not guarantee that
                        the type of the value is definitely a .NET reference type.

    pat ==           
      | null         -- the pattern corresponds to a null test. A compile-time error
                        will occur if local type inference does not ensure that
                        the value being matched against is a .NET reference type.

Here is an example where we do a null test in a pattern match:

let d = new OpenFileDialog() in 
  if d.ShowDialog() = DialogResult.OK then 
    match d.OpenFile() with 
    | null -> Printf.printf "Ooops... Could not read the file...\n"
    | stream -> ...
        let r = new StreamReader(stream) in 
        Printf.printf "The first line of the file is: %s!\n" (r.ReadLine());

Here is an example where we do a null test in an expression:

let myReader = new StreamReader(new FileStream("hello.txt")) in 
while true do Console.WriteLine(myStream.ReadLine() ?? raise End_of_file); done;

At the source language level null is not a valid value for other F# types, and if you attempt to use nullchecking constructs with types such as int or int list then the compiler will complain.] It will also complain if you do null checks or generate null values for a variable type 'a. However note that null will be used as a representation for some F# values, as described in the section on export interopability, though that is largely a choice made by the compiler.

On the whole F# code will often assume values .NET reference type values are non-null, so it is good style to eliminate the nullness and make it explicit as early as possible. You should only really need null checks at the API boundary. Strings (which are the .NET reference type System.String) will only be null if returned as such by a .NET API. String values returned by a .NET API should always be checked for nullness, since F# code generally assumes string values are non-null.

null is not otherwise part of the F# type system. Thus the compiler does not check that you insert null checks after API calls. If you do not insert a null check then just as in C# you will get a null pointer exception whenever you try to call a method on the object.

Aside: It is likely that a future version of F# will add some kind of refinement types to cover these circumstances, along with an analysis of the .NET libraries to determine nullness. Some other Microsoft Research projects are proposing and annotating common .NET libraries with nullness information, and F# would piggy-back off that.

Aside: It is possible to generate null values of any F# type through a number of backdoor routes, e.g. by taking the null object value and casting it to an F# type, or by using reflection to create a null value of an F# type, or by using other .NET languages. The runtime behaviour of such programs is unspecified and may raise a NullReferenceException. However, overall memory safety will not compromised as a result of this.

Advanced Topics

Advanced Topics: Resolving overloading

Documentation in progress.

Advanced Topics: Accessing generic types

Documentation in progress.