2

For reference in this example I have this Person type, although it could be any type:

public class Person
{
   public Person? Partner { get; set; }
   public Person? Child { get; set; }
}

Somewhere else I am checking if the person has a partner and children, but its not required to assign either.

Currently it looks like:

Person partner = null!;
if (person.Partner is not null)
{
   partner = person.Partner;
   //do stuff
}

if (person.Child is Person child)
{
   if (partner is not null)
   {
      //do one way
   }
   else
   {
      //do other way
   }
   //do stuff
}

Live example

I wanted it to look something along the lines:

if (person.Partner is Person partner)
{
   //do stuff
}

if (person.Child is Person child)
{
   if (partner is not null)
   {
      //use partner
   }
   //other stuff
}

Live Example

partner on the last example is considered declared outside of the if statement scope, but is not assigned. How could I, using this pattern, let the compiler know that its either assigned with the person.Partner reference or null!?

Much like the out keyword on the following example doesn't throw a "not assigned" error:

SomeMethod(out Person partner);
if(partner is not null) { }

Live Example

I am learning about the C# patterns and have looked into the documentation and feature proposal and there is no mention of what I was looking for, not even why it is declared outside of the scope preventing me from using the variable name? Even if I tricked my way around the pattern using it on the needed scope as shown here its not assigned.

Is the above first example the standard approach and only way to do it?

Barreto
  • 374
  • 2
  • 14
  • "Is the above first example the standard approach and only way to do it?" No. We did it differently before pattern matching was added to the language. Simply declare and set the variable in the scope you want it in (i.e., `Person? partner = person.Partner;` ... `if (partner != null) { ... }`). As to why, the people who make decisions about these kinds of things decided it would be that way. – Heretic Monkey Mar 22 '23 at 13:01
  • well yes I understand that's an option, I am referring by using patterns. that would be pretty much the same as my example without using the pattern matching. and the why its available outside the scope but not considered declared might have an answer, I dont know, maybe someone knows. I am not questioning the "people who make decisions" just trying to understand. – Barreto Mar 22 '23 at 13:10
  • The problem with `if (person.Partner is Person partner)` would be that `partner` would be classed as unassigned outside the scope of the `if`. For example, try this and check the compile error: `object obj = "Test"; if (obj is string text){ Console.WriteLine(text); } Console.WriteLine(text??"");` – Matthew Watson Mar 22 '23 at 13:18
  • Only the language designers would know the reason for this, but it does ensure that the variable introduced into the `if` is definitively assigned, and thus obviates the need for any further checks. – Matthew Watson Mar 22 '23 at 13:27
  • Your example is the same as in my examples, or not? But in the last "note" I declared on the scope and its still not declared. Btw I do not intend to use such example it is only for context and understanding. – Barreto Mar 22 '23 at 14:13
  • 1
    [The earliest design notes that I can find for this](https://github.com/dotnet/roslyn/issues/180) state that `The variables are definitely assigned only when true.` so that would appear to be an underlying reason for the behaviour. As to why that was decided - you'd have to ask the language designers or dig further on github. – Matthew Watson Mar 22 '23 at 14:58
  • Thanks for the link, I could dig a bit more in there. I assumed that it wasn't assigned because the compiler says, but that doesn't explain why its declared in a scope where it wont ever be used if I understood it right – Barreto Mar 22 '23 at 15:14
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/252696/discussion-between-barreto-and-matthew-watson). – Barreto Mar 22 '23 at 15:17

2 Answers2

1

The behavior you're seeing is very intentional, because it helps to treat variables like this in many, many cases where you only want to deal with a non-null value.

The decision to treat the variable as unassigned outside of the if statement is easy to explain: since a variable declared this way is inherently not null, you wouldn't want to allow its use in a place where it could be null. This provides compile-time safety to prevent people making a common mistake.

One could certainly argue that there's precedent for moving the variable scope inside the if block, similar to what happens with variables declared in a for statement's initializer. Perhaps the language designers felt the principle of least surprise was best served by preventing the reuse of variable names where the scopes might be ambiguous. C-based languages have for loop behaviors long before C# came along, whereas pattern matching is a new feature. It's also reasonable to imagine non-nested for loops are less logically coupled to one another, whereas if conditionals are more frequently used for control flow decisions that might be related to one another:

// A common pattern: we see these as two distinct steps.
for(int i = 0; i < someValue; i++)
{
  ...
}
for(int i = 0; i < someOtherValue; i++)
{
  ...
}
// This could be more confusing: 
// better to name the variables `parentPartner` and `childPartner`.
if(parent.Partner is Person partner)
{
  ...
}
else if(child.Partner is Person partner)
{
  ...
}

This decision also enables patterns like the following:

if (child.Parent is { IsLiving: true } responsiblePerson)
{
    Console.WriteLine("Child has a living parent to be their guardian");
}
else
{
    Console.WriteLine("Child is a ward of the state");
    responsiblePerson = theState;
}
// use `responsiblePerson` here.

If you're annoyed by the verbosity of your first solution, why not try this?

Person? partner = person.Partner;
if (partner is not null)
{
   // do stuff: the compiler will recognize `partner` is not null here
}

if (person.Child is Person child)
{
   if (partner is not null)
   {
      //do one way
   }
   else
   {
      //do other way
   }
   //do stuff
}
StriplingWarrior
  • 151,543
  • 27
  • 246
  • 315
  • Yes that example looks better than mine, IMO, but the question remains, you say to keep the variable scoped, but the variable is scoped outside the if statement, is it intentional? What is the purpose? It seems that will never be assigned out of the if statement – Barreto Mar 22 '23 at 16:08
  • 1
    @Barreto: I expanded my answer to discuss that. There are valid reasons the language designers could have gone either direction, but I've provided an example (albeit uncommon, in my experience) of a pattern that wouldn't be possible if the variable were scoped differently. – StriplingWarrior Mar 22 '23 at 16:22
  • @Barreto: from a design perspective, `if` has never introduced scopes of its own; the statement contained in the `if` need not be a block. An `x is y` pattern matching expression doesn't introduce a new scope either, as it's an expression (`bool hasPartner = person.Partner is Person partner` is meaningful, and legal). Changing semantics so that an `if` statement (or even all statements) combined with a pattern matching expression introduces a new scope would be awkward, since the result could no longer be described in terms of simpler constructs (declaration, then statement). – Jeroen Mostert Mar 22 '23 at 16:33
  • I have to check that "isLiving" statement example, if you can use it outside as explained is interesting, just wanted to point out that you mention `...as undeclared outside of the if...` but it is declared just not assigned, and following that line I wonder if that is so clear, since we could make null check like we do in the methods with `out` variables, and if they wouldn't want us to use the variable shouldn't be declared IMO just like you mention in the for loop. Although maybe the reason is really simple, as mentioned, that `if` doesn't have its own statement initialiser.. – Barreto Mar 22 '23 at 17:33
  • I had a look now at the compiled c# , [sharplab example](https://sharplab.io/#v2:D4AQTAjAsAULAKBTATgZwPYDsAEAHFGOAvNpogO7ZJpawDes2T2AcgIYC2i2JARAFLoAFpl4AaRs3htkAFzLIepClQJY6rTtz4BBTGV7YAvrCMBuWLACWAM2wAKfDUwA6aXIXYrqVc7wz5FABKeklsEAgATnsAEl46J0IXdi4jLx9ETABzNgATLMRc7Fl0bASAhWStI14gixgTOCaQAGZwsF9CUJhmVvCIAAZNLjLsAtkzbFRECeMlTABXABslgEJ63rbqQgB+KgqUUfHJ6dnGoyA===), and it converts the pattern to regular `obj != null` with the obj declared on the scope outside the if statement, similar to declaration on `for` but the compiler error is different, `if` is not assign, `for` doesn't exist. – Barreto Mar 22 '23 at 17:58
  • Good catch on my poor wording. I changed "undeclared" to "unassigned" there. – StriplingWarrior Mar 23 '23 at 15:01
1

I would argue that @StriplingWarrior answer covers the question itself but I would like to point several things out:

Currently it looks like: ...

I would say that correct variable declaration should look like:

Person? partner = null;

So the nullability info is represented correctly.

or even better just:

Person? partner = person.Partner;

and compiler will understand that:

if(partner is not null)
{
    Console.WriteLine(partner.SomeProp); // partner is not null here, no warnings
}

Much like the out keyword on the following example doesn't throw a "not assigned" error: ...

I would argue that this is an incorrect comparison, in case of out keyword the out parameter should be explicitly assigned some value (ideally a valid one, respecting the nullability info), i.e.:

void SomeMethod(out Person partner)
{
    partner = new Person();
}

Which obviously is not supported by the pattern matching (and syntax to support that potentially can be quite cumbersome).

Since C# does not automatically initialize local variables (i.e. Person p; can be used until assigned some value, see this answer why) you see the error.

P.S.

There is also option to use empty property pattern for null checks {} (see this answer):

if (person.Partner is {} partner)
{
   //do stuff
}
Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • I agree with 'Person? partner = person.Partner;' but would like to clarify that the 'out' mention was not really a comparison nor a proper example of usage. Its just to represent the behaviour and show that it could behave that way, at least in my limited understanding of things makes sense – Barreto Mar 25 '23 at 20:38