lysergic-acid
lysergic-acid

Reputation: 20050

Implicit operator issue with struct in C#

I am trying to define a helper class that acts as a trigger.

The behaviour is that when first tested for true/false, it should return true, and for all subsequent calls will be false (thus, will be used for one-time operations):

public struct Trigger
{
    private bool converted;
    private bool disposed;

    public static implicit operator bool(Trigger trigger)
    {
        if (trigger.disposed)
        {
            return false;
        }

        // Value will only be true once.
        if (!trigger.converted)
        {
            trigger.converted = true;
            trigger.disposed = true;
        }

        return trigger.converted;
    }
}

The issue is, that the implicit operator always returns true

When changing from struct to class it works fine.

Is there anything special about structs that i am missing here, with respect to defining implicit operators ?

Upvotes: 0

Views: 3413

Answers (2)

Jeppe Stig Nielsen
Jeppe Stig Nielsen

Reputation: 62002

The comments to your quetion already contain the answer.

The reason is that when a value type is passed as a parameter of a method (or of an "operator" in this case), a copy of the entire object is made. So the update of your fields converted and disposed happens to the copy of the object. That copy is not kept when control leaves the method.

If you made a ref parameter, that would help, but that is not allowed in operators (for good reasons), so it would look like:

// just to explain, not a recommended "solution"
public static bool ToBool(ref Trigger trigger)  // ByRef parameter
{
    .... // change 'trigger' here and return value
}

Of course an instance method (or instance property) could also help, see the answer by James Curran.


It is instructive to see what happens if the struct contains a field of a reference type. I choose the reference type bool[] which is an array type. I want an array of length one which can be mutated. When the struct Trigger is passed by value, the "value" of the field is a reference to the same array instance.

// just to explain, not a recommended "solution"
public struct Trigger
{
    bool[] referenceTypeField; // meant to always hold a length-one array of bools

    public static Trigger GetNew()
    {
        return new Trigger { referenceTypeField = new[] { false, }, };
    }

    public static implicit operator bool(Trigger trigger)
    {
        if (trigger.referenceTypeField == null)
            throw new InvalidOperationException("Always construct Trigger through the GetNew method");

        if (trigger.referenceTypeField[0])
            return false;

        trigger.referenceTypeField[0] = true;
        return true;
    }
}

Test this with:

var t = Trigger.GetNew();
Console.WriteLine(t); // True
Console.WriteLine(t); // False
Console.WriteLine(t); // False

Of course we see that we really just move the null problem to the field. The default(Trigger) is invalid because its field is a null reference.


There is a future version of C# coming up in which structs can have zero-parameter instance constructors, and can have field initializers for instance fields. With that version of C# you could have:

public struct Trigger
{
    readonly bool[] referenceTypeField = new[] { false, };  // ALLOWED!

    public static implicit operator bool(Trigger trigger)
    {
        if (trigger.referenceTypeField == null)
            throw new InvalidOperationException("Always construct Trigger properly");

        if (trigger.referenceTypeField[0])
            return false;

        trigger.referenceTypeField[0] = true;
        return true;
    }
}

It is often said that mutable structs are evil (search for threads on that here on Stack Overflow) because of issues like yours. The best advice is to abandon the idea of having a mutable struct (and not use any of my example code above).

Upvotes: 2

James Curran
James Curran

Reputation: 103555

The cast-to-bool operator is forcing a copy, so it always sees a new Trigger object.

Force it to use the same one by making it an instance method. (I did it as a property, but it could be a method as well). I also simplified it a bit.

struct Trigger
{
    private bool triggered;

    public bool IsFresh
    { 
        get
        {
            bool t = this.triggered;
            this.triggered = true;
            return !t;
        }
    }
}   


void Main()
{
    Trigger T = new Trigger();

    var a = T.IsFresh;
    var b = T.IsFresh;
    var c = T.IsFresh;

    Console.WriteLine("{0} {1} {2}", a,b,c);
}

Upvotes: 1

Related Questions