Saturday, June 27, 2009

Revised Generic class ToString() Method

After reading this great blog by Rinat Abdullin, I decided to revise my Generic ToString solution somewhat.

Here is the complete class:


/// <summary>
/// Creates a string of this format: MemberType MemberName=MemberValue
/// Usage:
/// <code>
/// string testMember = "testing";
/// Console.WriteLine(Member.State(() => testMember));
/// </code>
/// Writes ' string testMember="testing" ' to the Console.
/// </summary>
public static class Member
{
public static string State<T>(Func<T> expr)
{
var member = ExtractMemberFromLambdaExpression(expr);

Type memberType = GetTypeOfMember(member);

string contents = ExtractContentsFromLambdaExpression(expr);

return string.Format("{0} {1}={2}",memberType.Name, member.Name, contents);
}

static string ExtractContentsFromLambdaExpression<T>(Func<T> expr)
{
if (expr() == null) {
return "NULL";
}

string contents = string.Empty;
if (expr().GetType().IsArray) {
foreach (var item in (expr() as Array)) {
contents += item.ToStringNullSafe() + ", ";
}
contents = contents.Trim().TrimEnd(',');
} else {
contents = expr().ToString();
}

return contents;
}

static MemberInfo ExtractMemberFromLambdaExpression<T>(Func<T> expr)
{
// get IL code behind the delegate
var il = expr.Method.GetMethodBody().GetILAsByteArray();
// bytes 2-6 represent the member handle
var memberHandle = BitConverter.ToInt32(il, 2);
// resolve the handle
return expr.Target.GetType().Module.ResolveMember(memberHandle);
}


static Type GetTypeOfMember(MemberInfo member)
{
Type memberType;
if (member.MemberType == MemberTypes.Field) {
memberType = GetFieldType(member as FieldInfo);
}
else if (member.MemberType == MemberTypes.Property) {
memberType = GetPropertyType(member as PropertyInfo);
}
else {
memberType = typeof(object);
}
return memberType;
}

static Type GetFieldType(FieldInfo fieldInfo)
{
return fieldInfo.FieldType;
}

static Type GetPropertyType(PropertyInfo propertyInfo)
{
return propertyInfo.PropertyType;
}
}

As you can see from the supplied example in the XML documentation header, the use has become much simpler.
It is now using Lambda expressions in order to return its values.

This method uses strongly-typed reflection on the passed in lambda expression in order to obtain type and name of the field and invokes the ToStringNullSafe() method on the expression itself in order to obtain the value from the field.

In case you are wondering about the ToStringNullSafe() method - I created these in my previous blog.

Fortunately using the new Member.State() method is a lot easier than understanding the details, but I encourage you to read Rinat Abdullin's blog if you want to find out more. All I needed to know in order to implement this improved version, I found in there.

The big advantage of this new solution compared to the previous one, is, that now we don't have to implement an interface nor worry about the order of declaration of our member variables.
Additionally, they don't even have to be member variables. The new solution works for almost any object that implements a ToString() method.

As you can see from the ExtractContentsFromLambdaExpression method, if it encounters an array, it will return the contents of each value in the array.
It will not however return anything useful when it encounters a collection that is not an array, or any object, that doesn't implement a meaningful ToString() method. So think about what you pass into it.

Here a short example of how to use it to print information about a class:


public class PrintMyInfo
{
string infoString;
int counter;
double[] arrayOfValues;
List<string> lstString;
public PrintMyInfo()
{
this.infoString = "Info";
this.counter = 12;
this.arrayOfValues = new double[] { 0.3, 0.5, 23.2};
lstString = new List<string>();
lstString.Add("item1");
lstString.Add("item2");
}

public override string ToString()
{
var sb = new StringBuilder();
sb.AppendLine("PrintMyInfo: ");
sb.AppendLine(Member.State(() => this.infoString));
sb.AppendLine(Member.State(() => this.arrayOfValues));
sb.AppendLine(Member.State(() => this.counter));
sb.AppendLine(Member.State(() => this.lstString.ToArray()));
return sb.ToString();
}
}

Note how we need to copy the List to an array in order to get meaningful output.


Console.WriteLine(new PrintMyInfo().ToString());
produces the following output:
PrintMyInfo:
String infoString=Info
Double[] arrayOfValues=0.3, 0.5, 23.2
Int32 counter=12
List`1 lstString=item1, item2
Should we ever need to change any variable names in our class, the refactoring tool, will adjust our ToString() method automatically, since nothing is hardcoded in a string except the class name.

Labels: , ,

0 Comments:

Post a Comment

Subscribe to Post Comments [Atom]

<< Home