DEV Community

Simon
Simon

Posted on

Make ToString() human readable in C#

Data Class in Kotlin

If you have ever used Kotlin and it’s data class, I’m certain you’ve stumbled upon one of its amazing features when calling the ToString() method.

The term DataClass in this Article just refers to my own implementation of overriding the ToString() method and not any other functionalities of the Kotlin data class! Bad naming on my side.

Basically what it does is presenting the data within a class in a human-readable format.
Below is an example in Kotlin that shows both types:

class Person(val name: String, val age: Int)
// toString -> de.calucon.sample.Person@63e31ee

data class Person(val name: String, val age: Int)
// toString -> Person(name=Simon, age=21)

Potential Usage

This can be a very useful feature in some circumstances, especially during development. Of course, you can use the integrated Debugger which shows you every single variable and its members, but for this, you also have to pause the execution of the application.
Where this might be beneficial to you depends on what types of projects you’re working on.

I’m currently working on a WPF Application that gets its data from a TCP connection with the server. Having the parsed data from the TCP socket printed out to the Debug window while trying to break the application is a huge benefit because I’m able to observe if the correct data got transmitted or if it didn’t even reach the transmitter.

Data Class implementation in C#

C# by default does not provide a data class as Kotlin does.
This is how it looks in C# when using a class or a struct:

public class Person
{
    public string Name;
    public int Age;
}

var person = new Person()
{
    Name = "Simon",
    Age = 21
};

Console.WriteLine("Person: {0}", person);
// Console Output: Person: Calucon.Sample.Person

What Console.Write does is calling the ToString() method for each object/parameter.
We could now override the default ToString() method for each class and return a string that looks like the one from Kotlin, but this can be very time consuming and we have to deal with one big enemy: refactoring of variable names. This forces us to change the return value of the ToString() method to make the new field/property name match the output.

Implementation

First – Query all fields and properties from our Person class using Reflection

// define which fields/properties to get
var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

foreach (var prop in GetType().GetProperties(flags))
{
    // code...
}

foreach (var field in GetType().GetFields(flags))
{
    // code...
}

Second – Get the field/property name and its value
(code within the foreach loops above)

// Property
string name = prop.Name;
object val = prop.GetValue(this);

// Field
string name = field.Name;
object val = field.GetValue(this);

GetType() returns the Type of the class implementing our DataClass, therefore the keyword this refers to the class that implements our DataClass.

Third – Let it look like Kotlin

Kotlin by default looks like this:
Type(field0=value0, field1=value1, ...)

private void Analyze(ref StringBuilder sb, string name, object val)
{
    sb.AppendFormat("{0}={1}, ", name, val);
}

This would basically do the trick, but Kotlin also translates Arrays, Lists, etc.
In C# we have an interface ICollection which we're going to use to identify Arrays, Lists, Dictionaries, …

private void Analyze(ref StringBuilder sb, string name, object val)
{
    // if it's an ICollection, parse the collection
    if (val is ICollection) val = ConvertCollection(val as ICollection);
    sb.AppendFormat("{0}={1}, ", name, val);
}

// Analyze any ICollection
private string ConvertCollection(ICollection enumerable)
{
    var sb = new StringBuilder();
    foreach (var item in enumerable) 
    {
        sb.AppendFormat("{0}, ", item);
    }
    var data = sb.ToString().TrimEnd(',', ' '); // remove trailing ', '
    return string.Format("[{0}]", data);
}

Fourth – Exclude Fields created by the compiler
.NET generates some fields during compilation that are only for internal use. By using reflection we would also reveal these, but they do not provide us with anything useful.
They might look like this: <Array>k__BackingField

private void Analyze(ref StringBuilder sb, string name, object val)
{
    // exclude fields generated by the compiler
    if (name.EndsWith("k__BackingField")) return;
    if (val is ICollection) val = ConvertCollection(val as ICollection);
    sb.AppendFormat("{0}={1}, ", name, val);
}

Fifth – Create our DataClass that overrides the ToString() method
In this example, we’re overriding the default CultureInfo to InvariantCulture to compensate for different number formats.

public class DataClass
{
    public override string ToString()
    {
        // store current culture and override it with InvariantCulture
        var currentCulture = CultureInfo.CurrentCulture;
        CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

        var sb = new StringBuilder();
        // code from 'First' and 'Second'

        // reset culture info
        CultureInfo.CurrentCulture = currentCulture;

        // return final string and remove traling ', ' from the StringBuilder
        return string.Format("{0}({1})", 
            GetType().Name, 
            sb.ToString().TrimEnd(',', ' ')
        );
    }

    private void Analyze(ref StringBuilder sb, string name, object val)
    {
        // code from 'Fourth'
    }

    private string ConvertCollection(ICollection enumerable)
    {
        // code from 'Third'
    }
}

Code-Comparison

Kotlin

data class Person(val name: String, val age: Int)
// toString -> Person(name=Simon, age=21)

C#

public class Person
{
    public string name;
    public int age;
}
// toString -> Person(name=Simon, age=21)

DataClass Sourcecode

You can get the fully working class from my Gitlab here

  • Automatically generated fields by the compiler are excluded
  • Objects implementing ICollection are automatically processed. This includes objects like Arrays, Lists, Dictionaries, …
  • Numbers are represented using the InvarriantCulture of .NET

It also includes static fields and properties by default. If you want to remove them, just edit the DataClass on line 9 in the snippet:

// with static fields/properties
var flags = BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

// without static fields/properties
var flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

Final thoughts

Sometimes I get the feeling like I'm the only one who sometimes prefers to have all/some variables printed out in a human-readable format. I'm really curious how many of you are sometimes thinking the same and override the default ToString() method. I'm glad if this helps at least one single person :)

Also this is my first blog post ever. I'd be very grateful for any feedback you can give me to further improve my writing <3
You can read the original article here.

Top comments (0)