-2

In python, I've gotten into the habit of using variables inside a for loop outside of its "scope". For example:

l = ["one", "two", "three"]

for item in l:
    if item == "one":
        j = item

print(j)

You can't quite do this in C#. Here are the several attempts I made:

First attempt

I declare a variable j of type string, assign the selected item to it inside the foreach loop scope and then refer back to it once I exit the foreach loop scope:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };
        string j;       

        foreach (string item in l)
        {
            if (item == "one")
            {
                j = item;
            }
        }

        Console.WriteLine(j);
    }
}

The compiler throws an error:

Microsoft (R) Visual C# Compiler version 4.2.0-4.22252.24 (47cdc16a) Copyright (C) Microsoft Corporation. All rights reserved.

test.cs(19,27): error CS0165: Use of unassigned local variable 'j'

Second attempt

Moving the declaration inside the foreach is also no good, because the variable is not recognized outside of the scope at all:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };

        foreach (string item in l)
        {
            string j;

            if (item == "one")
            {
                j = item;
            }
        }

        Console.WriteLine(j);
    }
}

The compiler throws the following error:

Microsoft (R) Visual C# Compiler version 4.2.0-4.22252.24 (47cdc16a) Copyright (C) Microsoft Corporation. All rights reserved.

test.cs(20,27): error CS0103: The name 'j' does not exist in the current context

Third attempt:

Moving the declaration in the innermost scope and assigning the value to the variable results in a similar problem as the second attempt:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };

        foreach (string item in l)
        {

            if (item == "one")
            {
                string j = item;
            }
        }

        Console.WriteLine(j);
    }
}

The compiler complains because at line 19 the variable j is not recognized.

Microsoft (R) Visual C# Compiler version 4.2.0-4.22252.24 (47cdc16a) Copyright (C) Microsoft Corporation. All rights reserved.

test.cs(19,27): error CS0103: The name 'j' does not exist in the current context

The solution

One possible solution is as follows:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };
        string j = "test";

        foreach (string item in l)
        {

            if (item == "one")
            {
                j = item;
            }
        }

        Console.WriteLine(j);
    }
}

But I find this to be quite ugly and lacking robustness, because I have to assign some dummy value to j. For instance, perhaps the string "test" is recognized by other parts of my program and would make it behave in unexpected ways.

Question

Is there an elegant alternative to achieve this kind of behavior in C#, or am I missing something?

khelwood
  • 55,782
  • 14
  • 81
  • 108
user32882
  • 5,094
  • 5
  • 43
  • 82
  • 3
    First attempt is more correct, but the compiler is telling you that in certain cases (where your collection is empty), `j` will never be assigned to. Your solution is nearly there, but instead of `j="test`, I would use `j = null`, and then after your foreach, make sure j is not null before using it. – Neil Jul 01 '22 at 12:46
  • 1
    string j=""; would work too. using the Empty string - see https://stackoverflow.com/questions/263191/in-c-should-i-use-string-empty-or-string-empty-or-to-intitialize-a-string – doctorlove Jul 01 '22 at 12:47
  • @Neil that would mean I'd have to declare any variables I want to use in this manner as nullable (`string? j`, `int? j` or `char? j` for example... would that be correct? What is the difference between `null` and `string.Empty` in this case and does it matter? – user32882 Jul 01 '22 at 12:48
  • First attempt: if l is empty (initialized but no items), j will never be assigned a value. You need to assign a value to it in that case. The solution is to set it to a value (null, empty string, or some default value) before the loop. Then the compiler will be happy. `string j = "";` rather than `string j;` for example. – ProgrammingLlama Jul 01 '22 at 12:48
  • "am I missing something?" - that many of the design decisions in the C# language that produce different outcomes to even similar languages such as Java and C were due to experience of how often those patterns could lead to bugs. As is being hinted at, if the loop never runs, the variable is never assigned, and that (in other languages) can be a source of surprising and hard to track down bugs. – Damien_The_Unbeliever Jul 01 '22 at 12:51
  • The difference between null and empty, is really up to you. You could just as easily say that "XXX" is the value you use to determine if the item was not found. You want to make sure that the thing you use as the 'not found' value, doesn't exist in the collection, and `null` is a more conventional value to use. – Neil Jul 01 '22 at 12:52

4 Answers4

3

First attempt is more correct, but the compiler is telling you that in certain cases (where your collection is empty), j will never be assigned to. Your solution is nearly there, but instead of j="test", I would use j = null, and then after your foreach, make sure j is not null before using it

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };
        string j = null;

        foreach (string item in l)
        {

            if (item == "one")
            {
                j = item;
            }
        }

        if(j!=null) <-- check j has been assigned to, before using it.
        {
            Console.WriteLine(j);
        }
        else
        {
            Console.WriteLine("Item was not found");
        }
    }
}
Neil
  • 11,059
  • 3
  • 31
  • 56
2

Don't write the loop at all. What is your expectation for how often an element in your list should occur1? From your sample, it appears it should be at least once, but maybe exactly once.

Then pick the appropriate LINQ method that expresses your expectations clearly:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };
        string j = l.Single(item=>item=="one");       

        Console.WriteLine(j);
    }
}

The above will throw an exception if exactly one element doesn't match our criteria. You may instead pick First or Last (or with OrDefault) variants to express your requirements and expectations.


1 Or to put it another way, what is your expectations if the list is empty or no elements in the list match your search criteria?

Your python code appears to continue just assuming that neither of those are true. C# was designed to help you spot where shaky assumptions such as this are coming into play and prevent you from writing code with such assumptions.

Unassigned variables have, historically, been a massive source of hard to track down bugs because they either contain garbage (often but not always provoking an error quite soon) or a default value (which may look like a genuine value for quite some time), with the symptoms arising in a completely different location to the broken assumption.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • Far neater compared with for loops. – Peter Henry Jul 01 '22 at 12:59
  • I understand that in this case LINQ might be a better approach. But this question is really more about how to recreate a particular construct that I'm already familiar with in python to C#. I'll keep your answer for future reference though. Thanks. – user32882 Jul 01 '22 at 13:10
0

Maybe a little more sofisticated way to fix this is to initialize the variable like this:

string j = string.Empty;
0

You are not really using each of the values in the loop but just get the LAST one that matches. As an alternative you could use Linq and then query your list and supply a default value for example:

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<string> myList = new List<string>{"one", "two", "three"};
        string defaultValue = "whateverwant";
        string matchMe = "one";
        string j = myList.Where(item => item == matchMe)
            .DefaultIfEmpty(defaultValue)
            .Select(item => item).FirstOrDefault();
        Console.WriteLine(j);
    }
}

Using your first example you can set or check for a null for a default

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> l = new List<string> { "one", "two", "three" };
        string j = null;// or a default value string so we avoid the ternary

        foreach (string item in l)
        {
            if (item == "one")
            {
                j = item;
                break; // we found one so no need to keep looping
            }
        }

        j = string.IsNullOrEmpty(j)?"default":j
        Console.WriteLine(j);
    }
}
Mark Schultheiss
  • 32,614
  • 12
  • 69
  • 100