Return not behaving as i expect

Viewed 60

I'm solving the problem

here is the solution i made first

using namespace System.Linq
$DebugPreference = "Continue"
 
class kata{
    static [string] FirstNonRepeatingLetter ([String]$string_){
        [Enumerable]::GroupBy([char[]]$string_, [func[char, char]]{$args[0]}).Foreach{
            if ($_.Count -eq 1){
                Write-Debug ($_.key)
                return $_.Key 
            }
        }
        return "~~~"
    }
}
 
Write-Debug ([kata]::FirstNonRepeatingLetter("stress"))

it's return

DEBUG: t
DEBUG: r
DEBUG: e
DEBUG: ~~~

this is not what i expected I try this

using namespace System.Linq
$DebugPreference = "Continue"
 
class kata{
    static [string] FirstNonRepeatingLetter ([String]$string_){
        $groups = [Enumerable]::GroupBy([char[]]$string_, [func[char, char]]{$args[0]})
        foreach ($group in $groups)
        {
            if ($group.Count -eq 1){
                return $group.Key 
            }
        }
        return "~~~"
    }
}
 
Write-Debug ([kata]::FirstNonRepeatingLetter("stress"))

and then what I wanted to see

DEBUG: t

and now I don't understand why these codes work differently...

2 Answers

You're executing return inside a script block ({ ... }) you've passed to the .ForEach() array method method:

  • In script blocks passed as arguments (as opposed to blocks that are integral to statements such as if and foreach), return exits only the block itself.

  • Therefore, your .ForEach() enumeration continues after each return; that is, while execution inside the script block is terminated, execution continues with the next .ForEach() iteration.

There actually is no direct way to stop the enumeration of .ForEach() (by contrast, the related .Where() array method has an optional parameter that accepts a 'First' to stop enumerating after finding the first match).

You have two options:

  • Set a Boolean flag inside your script block to indicate whether a result has been found, and exit the script block right away if the flag is found to be set.

    • The obvious downside to this is that the enumeration always runs to completion.
  • Use a dummy loop so that you can repurpose a break statement to break out of the loop and thereby also out of the .ForEach() enumeration.

    • Note: Without the dummy loop, break would be looking up the entire call stack for a loop to break out of; if it finds none, execution would terminate overall - see this answer for more information.
  • Either way, you need a return statement outside to script block in order to return a value from your class method.

Here's how to implement the dummy-loop approach:

using namespace System.Linq
$DebugPreference = 'Continue'
 
class kata{
    static [string] FirstNonRepeatingLetter ([String]$string_){
      $result =  '~~~'
      do {  # dummy loop
        [Enumerable]::GroupBy([char[]]$string_, [func[char, char]]{$args[0]}).Foreach{
          if ($_.Count -eq 1){
                Write-Debug ($_.key)
                $result = $_.Key
                break  # break out of the dummy loop
            }
        }
      } while ($false)
      return $result
    }
}
 
Write-Debug ([kata]::FirstNonRepeatingLetter('stress'))

If you can live with doing that without a class, but a normal function instead, this should work:

function Get-FirstNonRepeatingCharacter ([string]$string) {
    ($string.ToCharArray() | Group-Object | Where-Object { $_.Count -eq 1 } | Select-Object -First 1).Name
}

Get-FirstNonRepeatingCharacter "sTress"  # --> T
Related