You’ve probably had to do this in C#. You get an object, you need to cast it to some type T and then fetch a property that is returned as an object, so you have to cast that to some other type U, so you can read the destination property.
For example, you have a ComboBoxItem, and you put some extra data in the Tag.
void AddComboBoxItem(Thing thing)
{
var item = new ComboBoxItem { Content = thing.Name, Tag = thing };
someComboBox.Items.Append(item);
}
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var thing = (Thing)((ComboBoxItem)((ComboBox)sender).SelectedItem)?.Tag;
...
}
In this case, when the selection changes, we ask the ComboBox for its currently-selected item, cast it to a ComboBoxItem, then get the Tag from it, then cast the Tag to the Thing that we were after in the first place.
In order to parse that expression, your eyes have to bounce back and forth because the casts are on the left, but the method calls and property accesses are on the right.
// 6 4 2 1 3 5
var thing = (Thing)((ComboBoxItem)((ComboBox)sender).SelectedItem)?.Tag;
You also have to pay attention to the parentheses, or what’s more likely to be the case, you simply trust that the parentheses are in the right place.
Enter the controversial extension method CastTo<T>.
namespace ObjectExtensions
{
static class ExtensionMethods
{
public static T CastTo<T>(this object o) => (T)o;
}
}
With this extension method, you can write your code as a straightforward left-to-right sequence.
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var thing = sender.CastTo<ComboBox>().SelectedItem.CastTo<ComboBoxItem>()?.Tag.CastTo<Thing>();
...
}
You can break up the long line for readability, and the fact that there are no large spans of parentheses makes the line breaks easier to place.
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var thing = sender
.CastTo<ComboBox>()
.SelectedItem
.CastTo<ComboBoxItem>()
?.Tag
.CastTo<Thing>();
...
}
Some people use the as operator instead of a cast, not because they actually care about the failure case (in which the result of the as is null), but because it lets them write things left-to-right.
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// 1 2 3 4 5 6
var thing = ((sender as ComboBox).SelectedItem as ComboBoxItem)?.Tag as Thing
...
}
This lets you read from left to right, but you still have to mind your parentheses. It looks a little prettier, but it also makes debugging harder.
You can write a similar extension method for as.
namespace ObjectExtensions
{
static class ExtensionMethods
{
public static T CastTo<T>(this object o) => (T)o;
public static T As<T>(this object o) where T : class => o as T;
}
}
This lets you change the above to
void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var thing = sender
.As<ComboBox>()
.SelectedItem
.As<ComboBoxItem>()
?.Tag
.As<Thing>();
...
}
I suspect that like my crazy thread-switching tasks, people are going to think either that this is a really cool trick, or it’s an offense against nature.
A potential pitfall of the "CastTo" that is avoided by the "As" is that "CastTo" does not have the same semantics as the cast operation. Another comment noted that CastTo does not respect user-defined conversions. But also, because CastTo boxes value types, the unboxing cast is required to be representation-preserving! Casting, say, a double to int would work differently with CastTo and an old-fashioned cast to int. Of course "As" avoids this by not working on any value types.
The larger point here is: the cast operation is yet another unfortunate example of a feature found in C that is frankly...
Here's a way to write it using LINQ:
var thing = (
from s in new int[] { 0 }
let comboBox = sender as ComboBox
let selectedItem = comboBox.SelectedItem as ComboBoxItem
where selectedItem != null
let tag = selectedItem.Tag as Thing
select tag
).FirstOrDefault();
It's easy to read, uses temporary variables so you can easily do null checks etc.
The only downside is having to create the initial array to enumerate.
Here’s an item I like use and share. Since an empty string often signals the same condition as a null, but cannot participate in null operators, I usually add this extension:
namespace Extensions
{
public static class StringExtensions
{
public static string NullIf(this string str) => string.IsNullOrWhitespace(str) ? null : str;
}
}
That allows this:
if (string.IsNullOrWhitespace(someString))
{
return “-No Entry-“;
}
else
{
return someString;
}
to become:
return someString.NullIfEmpty() ?? “-No Entry-“;
I’ve always found it wildly inconsistent that the BCL team decided to add Cast() to LINQ, but having a similar method on T is somehow controversial.