C# Data Class
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 overriding the ToString()
method and not any other functionalities of the Kotlin data class
Basically what it does is presenting the data in a human-readable format.
Below is an example 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#
But 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: API_Test.Person
What Console.Write
does is to call the ToString()
method of each object/parameter.
We can 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 value = prop.GetValue(this); // Field string name = field.Name; object value = 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 do the trick, but Kotlin also translates Arrays, Lists, etc.
In C# we have an interface ICollection
which we 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;