C# provides different ways of manipulating strings, which can be helpful when designing and implementing systems. So, in this article, we will delve deep into the distinctions among String, FormattableString, and IFormattable in C#. Besides that, we will test whether there are performance differences among them.
Without further ado, let’s get started!
String in C#
String
is an inbuilt data type that represents a sequence of characters. Furthermore, it is immutable, as we cannot change its values after we create it. For instance, when concatenating two string objects, we create a new string object to store the result.
First, let’s look at a simple string operation in C#:
public string StringExample(string studentName, int studentAge) { return $"My name is {studentName} and I am {studentAge} years old."; }
Here, we create a string object and use interpolation to insert our parameters.
In our test class, let’s set up some variables that we are going to use when verifying that our methods work efficiently:
private readonly FormattableStringsMethods _formattableStringMethods = new();
Consequently, let’s prove that we can manipulate our strings using some of the methods available in C#:
const string StudentName = "John"; const int StudentAge = 30; var sampleString = _formattableStringMethods.StringExample(StudentName, StudentAge); Assert.IsTrue(sampleString.Contains(StudentAge.ToString())); Assert.IsInstanceOfType(sampleString, typeof(string)); Assert.IsTrue(sampleString.Contains(StudentName));
FormattableString in C#
Sometimes, we may want to create a string but postpone its formatting until we request it. That is where the FormattableString class comes in handy. It allows us to generate string values and pass format arguments, which can be helpful when we want to defer the formatting process or pass strings to methods that handle the formatting.
In addition, the FormattableString
class has two properties: ArgumentCount
and Format
. The former gets the number of arguments the instance needs to format, while the latter returns the format string.
Besides these properties, the FormattableString
class has several methods which can come in handy when manipulating strings:
Method | Description |
---|---|
CurrentCulture | Takes a formattable string as an argument, formats it using the conventions of the current culture and returns a result string. |
Equals | Checks whether the current object is equal to a given object. |
GetArgument | This method takes an integer as a parameter and returns the argument at that position. |
GetHashCode | Acts as the default hash function in this class. |
GetType | Returns the type of the current instance. |
Invariant(FormattableString) | Uses the conventions of the invariant culture to format and return the result string. |
MemberwiseClone | Creates a shallow copy of the current object. |
ToString() | This method formats a composite format string with its arguments using the formatting conventions of the current culture and returns a result string. |
ToString(IFormatProvider) | It works in the same way as ToString() except for the fact that it uses the formatting conventions being provided by a specific culture. |
Next, let’s create a FormattableString
object to understand how the class works:
public FormattableString FormattableStringExample(string studentName, int studentAge) { return $"My name is {studentName} and I am {studentAge} years old."; }
Finally, within our unit test, we invoke several methods from the FormattableString
class to verify their functionality:
const string StudentName = "Sean"; const int StudentAge = 40; var sampleFormattableString = _formattableStringMethods.FormattableStringExample(StudentName, StudentAge); Assert.IsInstanceOfType(sampleFormattableString, typeof(FormattableString)); Assert.AreEqual(StudentName, sampleFormattableString.GetArgument(0)); Assert.AreEqual(StudentAge, sampleFormattableString.GetArgument(1)); Assert.AreEqual(sampleFormattableString.ArgumentCount, 2);
After this example, let’s check which scenarios the FormattableString
class can be useful.
Use FormattableString for Custom Formatting
We can use the FormattableString class to apply custom formats to some portions of our strings. To understand how this works, let’s implement an example that formats DateTime
values:
public FormattableString FormattableStringDateExample(DateTime currentDate) { return $"Today's date: {currentDate:D}"; }
Finally, we can verify that the current date has the correct format (long date):
var currentDate = DateTime.Now; var sampleFormattableString = _formattableStringMethods.FormattableStringDateExample(currentDate); Assert.IsInstanceOfType(sampleFormattableString, typeof(FormattableString)); Assert.IsInstanceOfType(sampleFormattableString.GetArgument(0), typeof(DateTime)); Assert.IsTrue(sampleFormattableString.ToString().Contains(currentDate.ToString("D")));
Use FormattableString for Logging
As software engineers, we know how important logging is when building and maintaining information systems. In logging scenarios, using FormattableString
can improve performance by deferring the actual formatting until it’s needed, which can reduce unnecessary formatting of log messages that might not be emitted due to log levels.
Let’s run through an example:
public FormattableString FormattableStringLoggingExample(LogLevel level, string message) { return $"[{DateTime.Now}] [{level}] {message}"; }
Here, we create a simple method that returns a formatted string including the level
and message
parameters.
Next, in our test, we can see that we can extract all the arguments and validate their types:
var level = LogLevel.Information; var message = "Logging method invoked"; var sampleFormattableString = _formattableStringMethods.FormattableStringLoggingExample(level, message); Assert.IsInstanceOfType(sampleFormattableString, typeof(FormattableString)); Assert.IsInstanceOfType(sampleFormattableString.GetArgument(0), typeof(DateTime)); Assert.IsInstanceOfType(sampleFormattableString.GetArgument(1), typeof(LogLevel)); Assert.IsInstanceOfType(sampleFormattableString.GetArgument(2), typeof(string));
Dynamic Message Composition
In some cases, we may want to return different strings if they meet specific conditions. We can take advantage of the FormattableString
class in such situations by separating the message structure from its content:
public FormattableString FormattableStringDynamicStringExample(string itemName, int itemCount) { var messageTemplate = $"You have {(itemCount > 1 ? "items" : "item")} in your cart."; return $"{itemCount}: {itemName}; {messageTemplate}"; }
If itemCount
is greater than one, we return “items” instead of “item” in our result string.
Lastly, we can proceed to ensure that it returns the correct output:
var itemName = "Laptop"; var itemCount = 3; var sampleFormattableString = _formattableStringMethods.FormattableStringDynamicStringExample(itemName, itemCount); Assert.IsInstanceOfType(sampleFormattableString, typeof(FormattableString)); Assert.AreEqual(itemCount, sampleFormattableString.GetArgument(0)); Assert.AreEqual(itemName, sampleFormattableString.GetArgument(1));
Therefore, it’s important to note that FormattableString
stores the format string and its arguments separately and does not represent the formatted string itself.
IFormattable Interface in C#
On the other hand, IFormattable is a C# interface that we can implement in our user-defined classes to provide formatting functionality to their instances.
So in order to learn how to implement the interface, let’s create a class that converts temperature values to different formats:
public class TemperatureFormat : IFormattable { private double _temperatureValue; public TemperatureFormat(double temperatureValue) { _temperatureValue = temperatureValue; } public string ToString(string format, IFormatProvider _) => format switch { "F" => $"{_temperatureValue * 9 / 5 + 32} F", "K" => $"{_temperatureValue + 273.15} K", _ => $"{_temperatureValue} C" }; }
First, we initialize a temperature value in our class constructor. Then, since we inherit from the IFormattable
interface, we have to implement the ToString()
method, which accepts two parameters: a format for the temperature value and a format provider.
Finally, we return a result string whose format depends on whether we want the Celsius, Kelvins, or Fahrenheit temperature values.
Next, let’s verify that the method works:
var temperature = new TemperatureFormat(20.0); var formattedKelvins = string.Format("Temperature: {0:K}", temperature); var formattedFahrenheit = string.Format("Temperature: {0:F}", temperature); var formattedCelsius = string.Format("Temperature: {0}", temperature); Assert.AreEqual("Temperature: 20 C", formattedCelsius); Assert.AreEqual("Temperature: 68 F", formattedFahrenheit); Assert.AreEqual("Temperature: 293.15 K", formattedKelvins);
Finally, we can prove that we can implement custom string formats depending on the required result strings with our example.
How String and FormattableString Perform in C#
Now that we have a grasp on the differences between string
and FormattableString
, let’s assess a benchmark result to compare performance:
| Method | Mean | Error | StdDev | Median | Rank | Gen0 | Allocated | |------------------------- |---------:|---------:|---------:|---------:|-----:|-------:|----------:| | FormattableStringExample | 22.62 ns | 0.406 ns | 0.838 ns | 22.25 ns | 1 | 0.0459 | 96 B | | StringExample | 68.94 ns | 1.462 ns | 1.564 ns | 68.74 ns | 2 | 0.0497 | 104 B |
From our results, we see that our FormattableString example performs better than our string example because we defer the formatting process until runtime. Although these methods work differently, in the long run, we expect them to have minimal performance differences.
Conclusion
In summary, this article examines the distinctions among String, FormattableString, and IFormattable in C#. Therefore, incorporating these concepts into our applications can enhance our string manipulation capabilities.