J
Jeff Grills
I am an experienced C++ programmer with over 12 years of development, and I
think I know C++ quite well. I'm changing jobs at the moment, and I have
about a month between leaving my last job and starting my new one. In that
time, I have decided to learn C#. I picked up the book, "Programming C#
(4th Edition)" recently and have read most of it. I've written about 1,500
lines of C# now, and I've run into the first really ugly thing I don't like
about the language - properties act like member variables, but they don't do
a good enough job of it.
Here's some sample code that demonstrates the issue I found. In computer
graphics, which is one of my programming specialties, colors are represented
by red, green, blue, and alpha components. Frequently these values are
expressed as floats, but they're also just as frequently expressed as bytes,
often with all 4 bytes packed into a single uint32. Most standard 3d
graphics pipelines (Direct3D and OpenGL) have the concept of a Material,
which indicates how light is reflected off the surface (basically you end up
multiplying the incoming light color by the material color in a
component-wise manner). Materials typically contain multiple colors -
Ambient, Diffuse, Emissive, and Specular, but I have included only the
diffuse color in this example to keep the code shorter.
In this sample, I have chosen to refactor the Material class and convert the
internal representation of the color from the 4-component byte
representation to the 4-component float representation.
namespace PropertyExample
{
public class FloatRgba
{
public float r = 1.0f;
public float g = 1.0f;
public float b = 1.0f;
public float a = 1.0f;
public FloatRgba()
{
}
public FloatRgba(float r, float g, float b, float a)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
public void Invert()
{
r = 1.0f - r;
g = 1.0f - g;
b = 1.0f - b;
a = 1.0f - a;
}
}
public class ByteRgba
{
public byte r = 255;
public byte g = 255;
public byte b = 255;
public byte a = 255;
public void Invert()
{
r = (byte)(255 - r);
g = (byte)(255 - g);
b = (byte)(255 - b);
a = (byte)(255 - a);
}
}
public class MaterialBefore
{
public ByteRgba diffuseColor = new ByteRgba();
public FloatRgba DiffuseColor
{
get
{
return new FloatRgba(
(float)diffuseColor.r / 255.0f,
(float)diffuseColor.g / 255.0f,
(float)diffuseColor.b / 255.0f,
(float)diffuseColor.a / 255.0f);
}
set
{
if (value.r < 0.0f || value.r > 1.0f || value.g < 0.0f ||
value.g > 1.0f || value.b < 0.0f || value.b > 1.0f || value.a < 0.0f ||
value.a > 1.0f)
throw new System.ArgumentOutOfRangeException();
diffuseColor.r = (byte)(value.r / 255.0f);
diffuseColor.g = (byte)(value.g / 255.0f);
diffuseColor.b = (byte)(value.b / 255.0f);
diffuseColor.a = (byte)(value.a / 255.0f);
}
}
}
public class MaterialAfter
{
public FloatRgba diffuseColor = new FloatRgba();
public FloatRgba DiffuseColor
{
get { return diffuseColor; }
set { diffuseColor = value; }
}
}
class Program
{
static void Main(string[] args)
{
MaterialBefore m0 = new MaterialBefore();
MaterialAfter m1 = new MaterialAfter();
System.Console.WriteLine("m0 before: " +
m0.diffuseColor.r.ToString());
System.Console.WriteLine("m1 before: " +
m1.diffuseColor.r.ToString());
m0.DiffuseColor.Invert();
m1.DiffuseColor.Invert();
System.Console.WriteLine("m0 after: " +
m0.diffuseColor.r.ToString());
System.Console.WriteLine("m1 after: " +
m1.diffuseColor.r.ToString());
}
}
}
In refactoring Material from MaterialBefore to MaterialAfter, the hope is to
prevent any client code from breaking when the internal representation of
the diffuse color is changed from bytes to floats. In the case of
m0.DiffuseColor.Invert(), the actual m0 object will be modified since the
DiffuseColor get method returns a reference to the actual object. In the
case of m1.DiffuseColor.Invert(), the temporary will be inverted and there
will be no change to the m1 object as the programmer expected. This
refactoring has introduced a very subtle and hard to find bug. It seems to
me that this will happen any time the programmer changes the internal
representation of a class such that a member is converted to a property
returning a temporary, or the existing property can no longer return a
member variable and is forced to return a temporary in a vain effort to keep
the API compatible.
Is there a decent way within the language to deal with this? I've been
pondering some solutions myself, but I don't really like any of them so far.
If C# had the notion of const like C++, I could make get accessors return
const objects so that I wouldn't have to worry about the client code trying
to change the object. I considered making every property get return a
temporary, but then properties have the same syntax as member variables but
act very definitely. I've also considered returning some sort of proxy
object that exposes a ByteRgba interface but internally holds a reference to
the FloatRgba version of diffuseColor and can modify that object when it
changes, but I think that would require fairly extensive changes to both
ByteRbga and FloatRbga, and that change may be very difficult if we were
dealing with more complex types.
In fact, thinking about the whole issue again, I don't think that the
problem is limited to properties - it would happen with a getDiffuseColor()
routine as well. Am I missing something obvious?
Thanks,
j
Jeff Grills
think I know C++ quite well. I'm changing jobs at the moment, and I have
about a month between leaving my last job and starting my new one. In that
time, I have decided to learn C#. I picked up the book, "Programming C#
(4th Edition)" recently and have read most of it. I've written about 1,500
lines of C# now, and I've run into the first really ugly thing I don't like
about the language - properties act like member variables, but they don't do
a good enough job of it.
Here's some sample code that demonstrates the issue I found. In computer
graphics, which is one of my programming specialties, colors are represented
by red, green, blue, and alpha components. Frequently these values are
expressed as floats, but they're also just as frequently expressed as bytes,
often with all 4 bytes packed into a single uint32. Most standard 3d
graphics pipelines (Direct3D and OpenGL) have the concept of a Material,
which indicates how light is reflected off the surface (basically you end up
multiplying the incoming light color by the material color in a
component-wise manner). Materials typically contain multiple colors -
Ambient, Diffuse, Emissive, and Specular, but I have included only the
diffuse color in this example to keep the code shorter.
In this sample, I have chosen to refactor the Material class and convert the
internal representation of the color from the 4-component byte
representation to the 4-component float representation.
namespace PropertyExample
{
public class FloatRgba
{
public float r = 1.0f;
public float g = 1.0f;
public float b = 1.0f;
public float a = 1.0f;
public FloatRgba()
{
}
public FloatRgba(float r, float g, float b, float a)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
public void Invert()
{
r = 1.0f - r;
g = 1.0f - g;
b = 1.0f - b;
a = 1.0f - a;
}
}
public class ByteRgba
{
public byte r = 255;
public byte g = 255;
public byte b = 255;
public byte a = 255;
public void Invert()
{
r = (byte)(255 - r);
g = (byte)(255 - g);
b = (byte)(255 - b);
a = (byte)(255 - a);
}
}
public class MaterialBefore
{
public ByteRgba diffuseColor = new ByteRgba();
public FloatRgba DiffuseColor
{
get
{
return new FloatRgba(
(float)diffuseColor.r / 255.0f,
(float)diffuseColor.g / 255.0f,
(float)diffuseColor.b / 255.0f,
(float)diffuseColor.a / 255.0f);
}
set
{
if (value.r < 0.0f || value.r > 1.0f || value.g < 0.0f ||
value.g > 1.0f || value.b < 0.0f || value.b > 1.0f || value.a < 0.0f ||
value.a > 1.0f)
throw new System.ArgumentOutOfRangeException();
diffuseColor.r = (byte)(value.r / 255.0f);
diffuseColor.g = (byte)(value.g / 255.0f);
diffuseColor.b = (byte)(value.b / 255.0f);
diffuseColor.a = (byte)(value.a / 255.0f);
}
}
}
public class MaterialAfter
{
public FloatRgba diffuseColor = new FloatRgba();
public FloatRgba DiffuseColor
{
get { return diffuseColor; }
set { diffuseColor = value; }
}
}
class Program
{
static void Main(string[] args)
{
MaterialBefore m0 = new MaterialBefore();
MaterialAfter m1 = new MaterialAfter();
System.Console.WriteLine("m0 before: " +
m0.diffuseColor.r.ToString());
System.Console.WriteLine("m1 before: " +
m1.diffuseColor.r.ToString());
m0.DiffuseColor.Invert();
m1.DiffuseColor.Invert();
System.Console.WriteLine("m0 after: " +
m0.diffuseColor.r.ToString());
System.Console.WriteLine("m1 after: " +
m1.diffuseColor.r.ToString());
}
}
}
In refactoring Material from MaterialBefore to MaterialAfter, the hope is to
prevent any client code from breaking when the internal representation of
the diffuse color is changed from bytes to floats. In the case of
m0.DiffuseColor.Invert(), the actual m0 object will be modified since the
DiffuseColor get method returns a reference to the actual object. In the
case of m1.DiffuseColor.Invert(), the temporary will be inverted and there
will be no change to the m1 object as the programmer expected. This
refactoring has introduced a very subtle and hard to find bug. It seems to
me that this will happen any time the programmer changes the internal
representation of a class such that a member is converted to a property
returning a temporary, or the existing property can no longer return a
member variable and is forced to return a temporary in a vain effort to keep
the API compatible.
Is there a decent way within the language to deal with this? I've been
pondering some solutions myself, but I don't really like any of them so far.
If C# had the notion of const like C++, I could make get accessors return
const objects so that I wouldn't have to worry about the client code trying
to change the object. I considered making every property get return a
temporary, but then properties have the same syntax as member variables but
act very definitely. I've also considered returning some sort of proxy
object that exposes a ByteRgba interface but internally holds a reference to
the FloatRgba version of diffuseColor and can modify that object when it
changes, but I think that would require fairly extensive changes to both
ByteRbga and FloatRbga, and that change may be very difficult if we were
dealing with more complex types.
In fact, thinking about the whole issue again, I don't think that the
problem is limited to properties - it would happen with a getDiffuseColor()
routine as well. Am I missing something obvious?
Thanks,
j
Jeff Grills