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 theToString()
method and not any other functionalities of the Kotlindata 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)