Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user-facing docs for Variant #38712

Closed
wants to merge 5 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions sdk/core/Azure.Core.Experimental/samples/Variant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# Azure.Variant

`Azure.Variant` is an implementation of a [tagged union](https://en.wikipedia.org/wiki/Tagged_union). It can hold both reference and value types and can be used to avoid boxing .NET primitives. The list of value types that Variant can hold without boxing is below.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Omit the locale from the URL. Also, avoid using directional terms like "below".

Suggested change
`Azure.Variant` is an implementation of a [tagged union](https://en.wikipedia.org/wiki/Tagged_union). It can hold both reference and value types and can be used to avoid boxing .NET primitives. The list of value types that Variant can hold without boxing is below.
`Azure.Variant` is an implementation of a [tagged union](https://wikipedia.org/wiki/Tagged_union). It can hold both reference and value types and can be used to avoid boxing .NET primitives. The following list contains value types that `Variant` can hold without boxing.


<details>
<summary>Value types that `Variant` won't box</summary>

- `byte`
- `byte?`
- `sbyte`
- `sbyte?`
- `bool`
- `bool?`
- `char`
- `char?`
- `short`
- `short?`
- `int`
- `int?`
- `long`
- `long?`
- `ushort`
- `ushort?`
- `uint`
- `uint?`
- `ulong`
- `ulong?`
- `float`
- `float?`
- `double`
- `double?`
- `DateTimeOffset`
- `DateTimeOffset?`
- `DateTime`
- `DateTime?`
- Enums

</details>

In principle, `Variant` is similar to `object`, but without boxing as described above. However, since it's not `object`, there are different APIs needed to achieve some of the same functionality, including:
scottaddie marked this conversation as resolved.
Show resolved Hide resolved

- [Assigning a value to `Variant`](#assign-a-value-to-variant)
- [Retrieving the value a `Variant` holds](#get-the-value-from-variant)
- [Working with `null` and `Variant`](#handling-nulls)

## Assign a value to Variant

`Variant` has implicit cast operators for each of the primitives listed above. This means you can assign a value of those types to `Variant` without casting, and a new `Variant` instance holding an unboxed copy of the value will be created.

```csharp
Variant v = 3L;

// v.Type is System.Int64
```

Since you can't have an implicit cast operator from `object`, for reference types, you must create a new instance of `Variant` and pass the value to the `Variant` constructor.
scottaddie marked this conversation as resolved.
Show resolved Hide resolved

```csharp
MemoryStream s = new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to include a using statement in this sample?

Suggested change
MemoryStream s = new();
using MemoryStream s = new();

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good callout. I don't want to add complexity to the samples, so I'll pick a type for the examples that isn't disposable to sidestep the issue. Thanks, @scottaddie!

Variant v = new(s);

// v.Type is System.IO.MemoryStream
```

`Variant` will box value types that aren't on the list above, because it only stores 16 bytes and user-defined value types might exceed this size. The one exception is for `enum`, which `Variant` can hold without boxing. Because we can't add implicit cast operators for unknown enum types, you must call the `Create` method to create a `Variant` that holds an enum without boxing.
scottaddie marked this conversation as resolved.
Show resolved Hide resolved

```csharp
Value v = Value.Create(System.Color.Blue);

// v.Type is System.Color
```

## Get the value from Variant

`Variant` has explicit cast operators for each of the primitives listed above, as well as to `string`. That means you can assign a `Variant` holding one of these types to a variable of that type with a cast:

```csharp
Variant v = 3;
int i = (int)v;
```

If you don't know the type of the value that a `Variant` is holding, you can read that from the `Variant.Type` property.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you don't know the type of the value that a `Variant` is holding, you can read that from the `Variant.Type` property.
If you don't know the type of the value that a `Variant` is holding, you can read that from the `Variant.Type` property:


```csharp
switch (v.Type)
{
case Type s when s == typeof(string):
Console.WriteLine($"string: {v}");
break;
case Type i when i == typeof(int):
Console.WriteLine($"int: {(int)v}");
break;
}
```

If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a potential caller, I'm not clear on whether the explicit cast operation, As<T>, or both will properly downcast or upcast e.g., Variant holds an int but would either approach upcast to a long? Or maybe it holds a long but small enough to fit into an int: will that be supported? What happens on overflow? I'd love to see these details explained, even if briefly, to know how to better use Variant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. You do explain that As<T> doesn't cast just below. But, still, my question stands about the explicit cast operator. And should we support that if not supported already? I see a lot of advantage in some scenarios rather than a potentially long switch statement like you show above.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good questions - let me clarify this!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type.
If you do know the type the `Variant` is holding, you can use the `As<T>` method to get its value as that type:


```csharp
MemoryStream s = new();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation: Our team is very inconsistent with how we instantiate in our code samples. Here, the target-typed new expression is used. In other locations, this same line of code may have been written in one of the following forms:

  • MemoryStream c = new MemoryStream();
  • var s = new MemoryStream();

This would be a good topic for the next .NET team sync. /cc: @jsquire

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, it was already codified a long time ago in our root .editorconfig and is that var is only usable when the rvalue is obvious - most often instantiation.

Though, we never have discussed the new() pattern.

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My personal preference is for not using var because so often I don't know what type a method is returning and I can't mentally parse the code until I replace those, which I can't do on a GH page. I'm entirely agnostic on new() vs. new TypeName(), and happy to vote with the majority on this. In this example, I used new() just to reduce the width of the sample.

Variant v = new (s);

MemoryStream streamValue = v.As<MemoryStream>();
```

If you call `Variant.As<T>` and the `Variant` isn't holding the type that you ask for, it will throw an `InvalidCastException`. To try to retrieve the value without risking an exception being thrown, you can use the `TryGetValue` method instead.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you call `Variant.As<T>` and the `Variant` isn't holding the type that you ask for, it will throw an `InvalidCastException`. To try to retrieve the value without risking an exception being thrown, you can use the `TryGetValue` method instead.
If you call `Variant.As<T>` and the `Variant` isn't holding the type that you ask for, it will throw an `InvalidCastException`. To try to retrieve the value without risking an exception being thrown, you can use the `TryGetValue` method instead:


```csharp
if (v.TryGetValue(out string s))
{
Console.WriteLine($"string: {s}");
}

if (v.TryGetValue(out int i))
{
Console.WriteLine($"int: {i}");
}
```

## Handling nulls
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid gerunds in headings.

Suggested change
## Handling nulls
## Handle nulls

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the thinking behind this rule?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I won't die on this hill, but there are 3 reasons:

  1. It's more difficult for non-English speakers to parse gerunds, since many of their languages don't have this word form. This factor matters if/when the content is localized. For example, if we decide to publish this content to learn.microsoft.com down the road.
  2. Gerunds are longer.
  3. The indicative verb form is the most direct (create a database vs. creating a database).


`Variant` handles nullable primitives without boxing them for value types on the supported list above. It also supports holding a reference type with a `null` value. In either of these cases, the variant's `Type` property will return `null`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I read this, any nullable value type will have a Type property value of null. Is that correct, or is it only when the value of the nullable value type is currently null?

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good. Let me clarify this. IIRC, the Type property is only null for a nullable value type when the value of the Nullable is null.

Before I checked, I had originally written that for value types we would return the actual type from the Type property (e.g. System.Nullable<System.Int32>), but for reference types we don't. I think this would be easy to implement but we don't do it today. We aren't able to store the type for reference types because it's either entirely unknown or would exceed our 128 bit storage limit.

What do you think? Would that be a better API experience?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think it would be much better to return the actual nullable type, if possible. The alternatives both seem worse (e.g. Type's value can change depending on the value, or all nullable value types return null)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking to COM's VARIANT, though, this is analogous to VT_NULL. Sure, it's an int?, but because it's null does it even have a type? And what can any caller do with it besides treat it as null? Or do you feel there's sufficient coverage with the IsNull property?

Copy link
Member Author

@annelo-msft annelo-msft Sep 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think it would be much better to return the actual nullable type, if possible.

I agree with this. Let me look at a) how hard it is to implement and b) what the implications are for type switching. I think in cases where we come in through a specific constructor (e.g. int? i = null; Variant v= i;), we can be clear about the type, and even though the value is null, if someone tried to do v = new MemoryStream() we could yell at them for type switching. It feels like there is a user intent here that we could support through the API. I'm not deep on VARIANT, but I know that people say it causes them nausea, so I don't think we have to preserve those rules here if we think there are better APIs to follow in a typesafe world.

I'm happy to look at examples where this gets interesting though, so we can think about whether a design choice like this might paint us into a corner. What are the pro/con cases here?


Working with nulls with Variant can be a little tricky for the following reasons. First, since a Variant instance holding `null` has a value (i.e., it's a Variant holding a `null`), any comparison to `null` will return false. Second, assigning a `Variant` to an instance that holds `null` could require you to call the `Variant` constructor and cast the `null` some type to avoid ambiguous method resolution errors. Because of this, there are some APIs that make working with `null` and `Variant` a little easier.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Grammar hound time 😄 : should you put "(i.e., it's a Variant holding a null)" in parens if you already preface it with "i.e.,"? I would think it'd just be:

Suggested change
Working with nulls with Variant can be a little tricky for the following reasons. First, since a Variant instance holding `null` has a value (i.e., it's a Variant holding a `null`), any comparison to `null` will return false. Second, assigning a `Variant` to an instance that holds `null` could require you to call the `Variant` constructor and cast the `null` some type to avoid ambiguous method resolution errors. Because of this, there are some APIs that make working with `null` and `Variant` a little easier.
Working with nulls with Variant can be a little tricky for the following reasons. First, since a Variant instance holding `null` has a value i.e., it's a Variant holding a `null`, any comparison to `null` will return false. Second, assigning a `Variant` to an instance that holds `null` could require you to call the `Variant` constructor and cast the `null` some type to avoid ambiguous method resolution errors. Because of this, there are some APIs that make working with `null` and `Variant` a little easier.

Alternatively, remove "i.e.," and just have it in parens. More of a question, though. I've never seen both used this way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to fix - I always have to fix up my grammar over multiple drafts 🙂


To check whether a Variant holds `null`, you can read the `IsNull` property:

```csharp
bool isNull = v.IsNull;
```

To assign the value a Variant holds to `null`, you can set it to `Variant.Null`:

```csharp
Variant v = Variant.IsNull;
Comment on lines +131 to +134
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You document Variant.Null but show Variant.IsNull. Not sure which is right given I haven't looked at the code in a while.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Will fix - thanks!

```
Loading