Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference between class-level and member-level self-identifier in F#?

Tags:

f#

Is there any semantic difference between class-level and member-level self-identifiers in F#? For example, consider this class:

type MyClass2(dataIn) as self =
    let data = dataIn
    do
        self.PrintMessage()
    member this.Data = data
    member this.PrintMessage() =
        printfn "Creating MyClass2 with Data %d" this.Data

Versus this class:

type MyClass2(dataIn) as self =
    let data = dataIn
    do
        self.PrintMessage()
    member this.Data = data
    member this.PrintMessage() =
        printfn "Creating MyClass2 with Data %d" self.Data

The only difference is that the implementation of PrintMessage references this in one vs. self in the other. Is there any difference in semantics? If not, is there a stylistic reason to prefer one over the other?

like image 816
Brian Berns Avatar asked Jun 20 '17 21:06

Brian Berns


2 Answers

There's no real semantic difference between the two. As a rule of thumb, I suggest going with your first example - prefer the identifier that's closer in scope, it makes it easier to read and refactor the code later. As a side note, people will usually use this both for class and member-level identifiers, in which case the member-level one shadows class-level one.

In these kind of scenarios, it's useful to look at the compiled code in a disassembler like ILSpy. If you do that, you'll find that the only difference is an extra null check that is inserted in self.Data case.

On the other hand, there is a difference between a class that uses a class-level identifier and one that doesn't (a series of initialization checks get inserted into all the class members). It's best to avoid having them if possible, and your example can be rewritten to not require one.

like image 123
scrwtp Avatar answered Nov 18 '22 14:11

scrwtp


As mentioned by scrwtp, this seems to be a commonly used identifier and it is my preference. Another very common one is x. I tend to use the class-level identifier when it's used multiple times throughout a class and of course when it's used in the constructor. And in those cases I would use __ (two underscores) as the member level identifier, to signify that the value is ignored. You can't use _ and actually ignore it as it's a compile error, but linting tools will often consider __ as the same thing and avoid giving you a warning about an unused identifier.

When you add a class-level identifier and don't use it you get a warning:

The recursive object reference 'self' is unused. The presence of a recursive object reference adds runtime initialization checks to members in this and derived types. Consider removing this recursive object reference.

Consider this code:

type MyClass() =
    member self.X = self

type MyClassAsSelf() as self =
    member __.X = self

type MyClassAsSelfUnused() as self = // <-- warning here
    member __.X = ()

This is what these classes look like after compiling/decompiling:

public class MyClass
{
    public Program.MyClass X
    {
        get
        {
            return this;
        }
    }

    public MyClass() : this()
    {
    }
}
public class MyClassAsSelf
{
    internal FSharpRef<Program.MyClassAsSelf> self = new FSharpRef<Program.MyClassAsSelf>(null);

    internal int init@22;

    public Program.MyClassAsSelf X
    {
        get
        {
            if (this.init@22 < 1)
            {
                LanguagePrimitives.IntrinsicFunctions.FailInit();
            }
            return LanguagePrimitives.IntrinsicFunctions.CheckThis<Program.MyClassAsSelf>(this.self.contents);
        }
    }

    public MyClassAsSelf()
    {
        FSharpRef<Program.MyClassAsSelf> self = this.self;
        this..ctor();
        this.self.contents = this;
        this.init@22 = 1;
    }
}
public class MyClassAsSelfUnused
{
    internal int init@25-1;

    public Unit X
    {
        get
        {
            if (this.init@25-1 < 1)
            {
                LanguagePrimitives.IntrinsicFunctions.FailInit();
            }
        }
    }

    public MyClassAsSelfUnused()
    {
        FSharpRef<Program.MyClassAsSelfUnused> self = new FSharpRef<Program.MyClassAsSelfUnused>(null);
        FSharpRef<Program.MyClassAsSelfUnused> self2 = self2;
        this..ctor();
        self.contents = this;
        this.init@25-1 = 1;
    }
}

Note that there is a check that a variable has been set in the constructor. If the check fails then a function is called: LanguagePrimitives.IntrinsicFunctions.FailInit(). This is the exception thrown:

System.InvalidOperationException: The initialization of an object or value resulted in an object or value being accessed recursively before it was fully initialized.

I guess the warning is there just so that you can avoid the slight overhead of an unnecessary runtime check. However, I don't know how to construct a situation where the error is thrown, so I don't know the exact purpose of the check. Perhaps someone else can shed light on this?

like image 4
TheQuickBrownFox Avatar answered Nov 18 '22 13:11

TheQuickBrownFox