Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Interfaces

An interface is a promise that a type can do certain things, with nothing said about how. It lists methods and properties, and has no fields and no code. Any type can take one on with an implements line, and a type can take on as many as it needs without giving up its one base.

'' A contract: what an encoder can do, with no data and no code. Any type
'' can take it on with an 'implements' line.
interface IEncoder
    function Name( ) as ustring
    function Encode( byref key as ustring, byval value as int32 ) as ustring
end interface

'' A plain type: no base, no vtable.
type IniEncoder
    implements IEncoder
    declare function Name( ) as ustring implements IEncoder.Name
    declare function Encode( byref key as ustring, _
                             byval value as int32 ) as ustring _
                             implements IEncoder.Encode
    sep as ustring = "="
end type

function IniEncoder.Name( ) as ustring
    return "ini"
end function

function IniEncoder.Encode( byref key as ustring, _
                            byval value as int32 ) as ustring
    return key & sep & value
end function

'' A class: a type that always extends Object.
class JsonEncoder
    implements IEncoder
    declare function Name( ) as ustring implements IEncoder.Name
    declare function Encode( byref key as ustring, _
                             byval value as int32 ) as ustring _
                             implements IEncoder.Encode
end class

function JsonEncoder.Name( ) as ustring
    return "json"
end function

function JsonEncoder.Encode( byref key as ustring, _
                             byval value as int32 ) as ustring
    return """" & key & """: " & value
end function

'' Written once, against the contract only.
sub Save( byval enc as IEncoder )
    print "as "; enc.Name( ); ":"
    print "  "; enc.Encode( "width", 640 )
    print "  "; enc.Encode( "height", 480 )
end sub

dim as IniEncoder ini
dim as JsonEncoder json
Save( ini )
Save( json )
print "an IEncoder value is"; sizeof( IEncoder ); " bytes"

Output

as ini:
  width=640
  height=480
as json:
  "width": 640
  "height": 480
an IEncoder value is 16 bytes

IEncoder is all contract: two members, every one public and abstract, so nothing else is written on them. Save is written against IEncoder alone. It runs unchanged for every encoder there is, including ones written after it.

implements IEncoder at the top of a type's body takes the contract on. Then each method says which member it provides: implements IEncoder.Name. IniEncoder is a plain type, with no base and no table of virtual methods; JsonEncoder is a class, a type that always extends Object. Both can implement an interface, and Save cannot tell them apart.

An interface value is a reference: the object's address and the table that type uses for this interface, 16 bytes. Save( ini ) passes a reference to ini, not a copy of it, and enc.Name( ) is called with .. Because it only refers to the object, the object must outlive it: a temporary, such as a function's result, cannot be converted to an interface (TKO1012), and delete through an interface is refused (TKO1013).

The mapping is explicit. A public Name with the right signature does not implement IEncoder.Name until its declaration says so:

interface IEncoder
    function Name( ) as ustring
end interface

'' Name is public and has the right signature, but nothing maps it.
type IniEncoder
    implements IEncoder
    declare function Name( ) as ustring
    sep as ustring = "="
end type

function IniEncoder.Name( ) as ustring
    return "ini"
end function
unmapped.tiko(7,16): error TKO1000: 'iniencoder' does not implement 'iencoder.name'; add 'implements iencoder.name' to one of its methods
     |
   7 |     implements IEncoder
     |                ^
unmapped.tiko(2,5): note: 'name' is required by interface 'iencoder', declared here
     |
   2 |     function Name( ) as ustring
     |     ^

Being explicit is what lets one type follow two contracts that use the same member name for different things: see Several interfaces.

An abstract type can serve as a contract too, and that is the right choice when the base should also supply code that derived types share. But a type extends exactly one base, so a contract written that way uses the base up.

Activities

  1. Add a CsvEncoder that writes width,640, and pass one to Save.
  2. Give IEncoder a third member, function Footer( ) as ustring, and read what the compiler says about each encoder until both provide it.
  3. Rename IniEncoder.Name to Label, keeping implements IEncoder.Name. What changes in the output?