4

Why does C# compiler generate error CS1612 for code that attempts to call indexer set accessor on immutable property?

using System;
using System.Collections.Generic;
using System.Text;

namespace StructIndexerSetter
{
    struct IndexerImpl {
        private TestClass parent;

        public IndexerImpl(TestClass parent)
        {
            this.parent = parent;
        }

        public int this[int index]
        {
            get {
                return parent.GetValue(index);
            }
            set {
                parent.SetValue(index, value);
            }
        }
    }

    class TestClass
    {
        public IndexerImpl Item
        {
            get
            {
                return new IndexerImpl(this);
            }
        }

        internal int GetValue(int index)
        {
            Console.WriteLine("GetValue({0})", index);
            return index;
        }
        internal void SetValue(int index, int value)
        {
            Console.WriteLine("SetValue({0}, {1})", index, value);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var testObj = new TestClass();
            var v = testObj.Item[0];

            // this workaround works as intended, ultimately calling "SetValue" on the testObj
            var indexer = testObj.Item;
            indexer[0] = 1;

            // this produced the compiler error
            // error CS1612: Cannot modify the return value of 'StructIndexerSetter.TestClass.Item' because it is not a variable
            // note that this would not modify the value of the returned IndexerImpl instance, but call custom indexer set accessor instead
            testObj.Item[0] = 1;
        }
    }
}

According to the documentation, this error means the following: "An attempt was made to modify a value type that is produced as the result of an intermediate expression but is not stored in a variable. This error can occur when you attempt to directly modify a struct in a generic collection, as shown in the following example:"

The error should not be produced in this case, since the actual value of the expression is not being modified. Note: Mono C# compiler handles the case as expected, successfully compiling the code.

Andrei
  • 1,015
  • 1
  • 11
  • 19
  • Submitted Microsoft Connect report: [link](https://connect.microsoft.com/VisualStudio/feedback/details/799884/compiler-error-cs1612-generated-inappropriately) – Andrei Sep 06 '13 at 19:33
  • I suggest you do some reading on why mutable structs are evil. You are falling into one of their traps. http://blogs.msdn.com/b/ericlippert/archive/2008/05/14/mutating-readonly-structs.aspx and http://stackoverflow.com/questions/441309/why-are-mutable-structs-evil – Brandon Sep 06 '13 at 19:40
  • Does the Mono compiler generate CS1612 in cases where it is more relevant, as in `struct S { public int A; } class C { static void M(List list) { list[0].A = 42; } }`? – Jeppe Stig Nielsen Sep 06 '13 at 20:08
  • 2
    Completely agree that mutable structs are evil, this code itself was an attempt at workaround for absence of named indexers in C#. The code does not attempt to mutate the struct itself, but, as Tim S. noted below, the compiler does not bother understanding what the code is actually doing. – Andrei Sep 06 '13 at 21:15
  • Jeppe: Mono does generate CS1612 where it is more relevant. error CS1612: Cannot modify a value type return value of `StructIndexerSetter.TestClass.Item'. Consider storing the value in a temporary variable – Andrei Sep 06 '13 at 21:19

1 Answers1

2

The C# compiler doesn't bother trying to understand what your code is actually doing in most cases. In this case, you're right that it will all work out, but this is some rather unusual code. As you may know, the following line:

var indexer = testObj.Item;

Results in the creation of a completely separate instance of IndexerImpl, since that's the way structs work. So normally when you change something inside it, with a line like indexer[0] = 1; you are changing indexer, not testObj.Item. Since testObj.Item[0] = 1; does the same thing, just without the named variable, your change is immediately discarded (or would be, if the value were stored inside the struct and not in the TestClass).

I'd say that not only are mutable structs evil, so are your pseudo-mutable structs here, and they should probably not be used in the real world. If you're looking for a real-world solution, try moving public int this[int index] inside TestClass, and access like testObj[0] = 1;.

Update given what you say in the comment on this answer, you should switch IndexerImpl to be a class (reference type), and write your properties in a way that you don't create lots of objects unnecessarily, e.g.

    private IndexerImpl _item;
    public IndexerImpl Item
    {
        get
        {
            return _item ?? (_item = new IndexerImpl(this));
        }
    }

This will reuse the instance, and will have practically no overhead.

Community
  • 1
  • 1
Tim S.
  • 55,448
  • 7
  • 96
  • 122
  • 1
    Of course moving it inside TestClass work, but this was an attempt to have _several_ named indexers, e.g. `testObj.PropA[idx]`, `testObj.PropB[idx]`. It all works just fine with reference type instead of value type, with the only difference that every access of this property results in a creation of a temporary object. Since this was going to be used in some high-throughput code, value type seemed more appropriate. – Andrei Sep 06 '13 at 21:27
  • 1
    Still requires one copy of IndexerImpl per instance of TestClass. Thanks for attempting to find an alternative solution, but that's not what I'm after. I know it's possible to make it work with a reference type, and I'm able to find an alternative myself. – Andrei Sep 07 '13 at 14:41
  • @AndreiTanas I really don't see why it'd be a problem to have one copy of `IndexerImpl` per instance, or even per indexed property. Creating and storing such a thing is really trivial. – Tim S. Sep 07 '13 at 17:19
  • 1
    Without going into too much detail, trivial things add up. The fact that you don't see why it's not a problem does not make it go away. And this is not answering the original question at all. – Andrei Sep 09 '13 at 16:56
  • @AndreiTanas usually, trivial things really are just trivial, and it's premature optimization (google that term if you're unfamiliar with the idea) to pretend they're not. Indexing is mostly just syntactic sugar. If it's causing a real performance issue, or overcomplicating your model, just make `GetValue` and `SetValue` `public` and call them directly. And I know I'm not really answering the question, because I'm not clear on what the question is and how to answer it. – Tim S. Sep 09 '13 at 17:49
  • 1
    I Agree that indexing is just syntactic sugar, the OP was not about optimization, it was about incorrect compiler behaviour. Comment about trivial things adding up was in response to you suggestion of creating a copy of `IndexerImpl` per instance of `TestClass`. You do realize that the example was simplified and specifically written for the post? I _know_ how to write equivalent code using method calls instead of indexers and did not ask what needs to be optimized and how. Your snide remarks and uncalled advices are childish and not being useful at all. – Andrei Sep 10 '13 at 01:46
  • Sorry, didn't mean to be snide or childish. More to the point about the compiler behavior: I wonder what the C# spec has to say about this? It's strange to me that Microsoft's C# compiler would do something different here than Mono's. Either the spec leaves it open to interpretation, or one or more of the compilers aren't meeting the spec. – Tim S. Sep 10 '13 at 12:01