views:

178

answers:

4

I disassembled the .NET 'System' DLL and looked at the source code for the variable classes (string, int, byte, etc.) to see if I could figure out how to make a class that could take on a value. I noticed that the "Int32" class inherits the following: IComparable, IFormattable, IConvertible, IComparable, IEquatable.

The String and Int32 classes are not inheritable, and I can't figure out what in these inherited interfaces allows the classes to hold a value. What I would want is something like this:

public class MyVariable : //inherits here
{
     //Code in here that allows it to get/set the value
} 

public static class Main(string[] args)
{
     MyVariable a = "This is my own custom variable!";
     MyVariable b = 2976;

     if(a == "Hello") { }
     if(b = 10) { }
     Console.WriteLine(a.ToString());
     Console.WriteLine(a.ToString());
}
A: 

"int" and "string" are built-in types. You won't be able to make one just like it that has no discernible fields. However, you can make something that looks and behaves almost exactly like a built-in type.

The best example is Nullable<T>. This provides a nullable version of all value types and can be assigned just like it's built in:

int? x = 0;
int? y = null;

The way this works is Nullable<T> overrides the explicit and implicit casts. This way you can assign a built-in type like int to a custom Nullable<int> and code within Nullable<int>.op_Implicit handles the conversion transparently.

Here's a complete example:

public struct MyVariable
{
    private int _intValue;
    private string _stringValue;

    public override bool Equals(object obj)
    {
        if (!(obj is MyVariable))
        {
            return false;
        }
        var y = (MyVariable) obj;
        return _stringValue == y._stringValue && _intValue == y._intValue;
    }

    public override int GetHashCode()
    {
        return (_stringValue ?? _intValue.ToString()).GetHashCode();
    }

    public override string ToString()
    {
        return _stringValue ?? _intValue.ToString();
    }

    public static implicit operator MyVariable(string value)
    {
        return new MyVariable { _stringValue = value };
    }

    public static implicit operator MyVariable(int value)
    {
        return new MyVariable { _intValue = value };
    }

    public static bool operator==(MyVariable variable, string value)
    {
        return variable._stringValue == value;
    }

    public static bool operator ==(MyVariable variable, int value)
    {
        return variable._intValue == value;
    }

    public static bool operator !=(MyVariable variable, string value)
    {
        return variable._stringValue == value;
    }

    public static bool operator !=(MyVariable variable, int value)
    {
        return variable._intValue == value;
    }

    public static void Test()
    {
        MyVariable a = "This is my own custom variable!";
        MyVariable b = 2976;

        if (a == "Hello") { }
        if (b == 10) { }
        Console.WriteLine(a.ToString());
        Console.WriteLine(a.ToString());
    }
}
Sam
Nullable actually has some extra magic behind the casts that you can't emulate in C#: lifted operators.
Martinho Fernandes
Thanks! This is exactly what I was looking for.
amerninja13
+6  A: 

You can do that by overloading operators. There is a tutorial on MSDN.

Let's say you want a type that can be either a string or an int (like Haskell's Either):

public sealed class StringOrInt32
{
    private string stringValue;
    private int int32Value;
    private bool isString;

    public bool IsString { get { return isString; } }
    public bool IsInt32 { get { return !isString; } }

    public string StringValue
    {
        get
        {
            if(!isString) throw new InvalidOperationException();
            return stringValue;
        }
    }

    public int Int32Value
    {
        get
        {
            if(isString) throw new InvalidOperationException();
            return int32Value;
        }
    }

    public StringOrInt32(string value)
    {
        isString = true;
        stringValue = value;
    }

    public StringOrInt32(int value)
    {
        isString = false;
        int32Value = value;
    }

    // Allows writing this:
    // StringOrInt32 foo = "Hello world!";
    public static implicit operator StringOrInt32(string value)
    {
        return new MyVariable(value);
    }

    // Allows writing this:
    // StringOrInt32 foo = 42;
    public static implicit operator StringOrInt32(int value)
    {
        return new MyVariable(value);
    }

    // Allows writing this:
    // StringOrInt32 foo = "Hello world!;
    // string bar = (string)foo;
    // Though foo.StringValue directly would be better
    public static explicit operator string(StringOrInt32 value)
    {
        return value.StringValule;
    }

    // Allows writing this:
    // StringOrInt32 foo = 42;
    // int bar = (int)foo;
    // Though foo.Int32Value directly would be better
    public static explicit operator int(StringOrInt32 value)
    {
        return value.Int32Value;
    }

    public static bool operator==(StringOrInt32 left, StringOrInt32 right)
    {
        if(left.IsString != right.IsString)
            return false;
        if(left.IsString)
            return left.StringValue == right.StringValue;
        else
            return left.Int32Value == right.Int32Value;
    }

    public static bool operator!=(StringOrInt32 left, StringOrInt32 right)
    {
        return !(left == right)
    }

    // Don't forget to override object.Equals(), object.GetHashCode(),
    // and consider implementing IEquatable<StringOrInt32>
    // Also, null checks, etc
}
Martinho Fernandes
Thanks! Like one of the other answers, this is exactly what I was looking for.
amerninja13
+3  A: 

The built-in types in C# are handled "specially" in that the literal value 1234 in code is defined as being of type System.Int32, and the literal value "some string" is defined as being of type System.String.

So in order to support the kind of code you want, you're going to need to provide conversion operators that can convert from int and string (and whatever else) to your type. Have a look at the conversion operators topic in MSDN.

Dean Harding
+1  A: 

Generally you want to make a new type to store a new representation of data. In your example above you are using MyVariable to store strings - something the string type is already good at doing.

If you want to store a different type of data, such as a combination of string and int in one easy to send around package you would do it as you've started above:

public class MyVariable
{
    public string Name { get; set; }
    public int Age { get; set; }
} 

Then:

MyVariable personOne = new MyVariable { Name = "John", Age = 34 };
MyVariable personTwo = new MyVariable { Name = "Joe", Age = 312 };

Interfaces such as IComparable and IFormattable allow your new type of be used in specific ways, for instance IComparable can be passed to sorted lists as it is capable of comparing its self to another instance and 'ranking' them accordingly.

Michael Shimmins