« Stupid C# Tricks | Main | My Old Games Live »

May 24, 2007

Comments

ionous

fyi: just tested.

while i can set an instance readonly by passing in a instance value to the SetField(), and non readonly statics work regardless if i pass null or an instance in the first param:
i cannot set an readonly static value regardless of the parameter.

nicely no exception seems to get raised either.

that seems like a bug, but possibly , since it's a readonly static, the complier assumes that the value can be evaluated completely at compile time, and makes it, essentially, const.

( a friend-like container class could still meet your needs -- you can drop the preprocessor foo if you don't need / want it. :)

ionous

sorry - i take that back -- i got up to get a cup of coffee, looked at the debugger, and low and behold: the value in the read only static had changed, but simple assignments using it still result in the old value!
ie.
class Test { public readonly static value=5.0f };
fi.SetValue( null, 10.0f );
float res= Test.value;

in the watch window:
* Type.staticvalue=10;
* res= 5;

but even better:
calling a custom function:
static void F() { float localres= Test.value; };
fi.SetValue( null, 10.0f );
F();

results in localres = 10.0f!

the compiler may be optimizing local assignments incorrectly.

you *may* therefore be able to do what you want if you are careful.....

Ryan Schneider

What about this?


class Enemy
{
static private int _maxEnemies = 4;
static public int MaxEnemies
{
get { return _maxEnemies; }
#ifdef UNIT_TEST_BUILD
set { _maxEnemies = value; }
#endif
}
}

Then define UNIT_TEST_BUILD only when creating your unit tests.

Jeff Ward

So I did a bit of investigating and found your problem. There's actually an explicit IL instruction which loads a static field onto the stack which is being optimized for the read only variable.

Here's the relevant code I'm testing:

Type enemyType = typeof(Enemy);
FieldInfo sMaxEnemiesField = enemyType.GetField("sMaxEnemies");
sMaxEnemiesField.SetValue(null, 100); Console.WriteLine(Enemy.sMaxEnemies.ToString());

Here's the IL that results starting with the SetValue call (from Reflector):
L_006b: callvirt instance void [mscorlib]System.Reflection.FieldInfo::SetValue(object, object)
L_0070: nop
L_0071: ldsfld int32 ReflectionTest.Enemy::sMaxEnemies
L_0076: stloc.s CS$0$0000
L_0078: ldloca.s CS$0$0000
L_007a: call instance string [mscorlib]System.Int32::ToString()
L_007f: call void [mscorlib]System.Console::WriteLine(string)

The ldsfld int32 call at L_0071 gets compiled into the following assembly(from the debugger):
mov dword ptr [esp+8],5
lea ecx,[esp+8]
call 786ED520

Instead of moving an address, the JIT optimized by moving the value of 5 on to the stack. My guess is that because the variable is read only, the JIT assumed that optimization was safe. In my mind, changing the variable this way is similar to getting the address of a const in C++ and then changing the value. 90% of the time, it’s not going to actually change the value throughout the code.

However, if you can live without the optimization of read only, you can make the variable private, provide a public set method, and then change it through reflection through this:
FieldInfo sMaxEnemiesField = enemyType.GetField("sMaxEnemies", BindingFlags.NonPublic | BindingFlags.Static);
sMaxEnemiesField.SetValue(null, 100);

It results in the same IL, but the compiled assembly is a bit more involved.
call dword ptr ds:[009195B8h]
mov esi,eax
mov dword ptr [esp+8],esi
lea ecx,[esp+8]
call 786ED550

But it will result in 100 being the result.

Jamie Fristrom

Jeff for president.

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

Your Information

(Name is required. Email address will not be displayed with the comment.)

Jamie's Bragging Rights

  • Spider-Man 2
    The best superhero games of all time Game Informer
    Top five games of all time Yahtzee Croshaw
    Top five superhero games of all time MSNBC
    Top 100 PS2 games of all time Official Playstation 2 Magazine
    1001 Games You Must Play Before You Die Nomination for Excellence in Gameplay Engineering Academy of Interactive Arts & Sciences
  • Schizoid
    Penny Arcade PAX 10 Award
    Nominated for XBLA Best Original Game
    Nominated for XBLA Best Co-Op Game