admin管理员组

文章数量:1316363

I have a method like below.

Product[] productArray = {
                new Product { Name="Kayak",Price=275M},
                new Product { Name="Lifejacket",Price=375M},
                new Product { Name="SoccerBall",Price=475M},
                new Product { Name="CornerBall",Price=775M}
            };
decimal arrayTotal = productArray.FilterByPrice(700).TotalPrices();

This will call Extension Methods FilterByPrice and TotalPrices.

public static decimal TotalPrices(this IEnumerable<Product?> products)
{
    decimal total = 0;
    foreach (Product? prod in products)
    {
        total += prod?.Price ?? 0;

    }
    return total;
}

public static IEnumerable<Product?> FilterByPrice(this IEnumerable<Product?>productEnum,decimal minimumPrice)
{
    foreach(Product? prod in productEnum)
    {
        if ((prod?.Price ?? 0) >= minimumPrice)
        {
            yield return prod;

        }
    }
}

My first question:

decimal arrayTotal = productArray.FilterByPrice(700).TotalPrices();

How the compiler chose the Total Prices method for execute first instead of former method?

My second question: While I debugged the code, I understood TotalPrices method is getting called First, and it goes to below line foreach (Product?prod in products)

After that all of sudden, flow is getting transferred to FilterByPrice method. Then yield will return the value? How?

I want to know how this happened all of a sudden. Is there any internal working I am missing?

I have a method like below.

Product[] productArray = {
                new Product { Name="Kayak",Price=275M},
                new Product { Name="Lifejacket",Price=375M},
                new Product { Name="SoccerBall",Price=475M},
                new Product { Name="CornerBall",Price=775M}
            };
decimal arrayTotal = productArray.FilterByPrice(700).TotalPrices();

This will call Extension Methods FilterByPrice and TotalPrices.

public static decimal TotalPrices(this IEnumerable<Product?> products)
{
    decimal total = 0;
    foreach (Product? prod in products)
    {
        total += prod?.Price ?? 0;

    }
    return total;
}

public static IEnumerable<Product?> FilterByPrice(this IEnumerable<Product?>productEnum,decimal minimumPrice)
{
    foreach(Product? prod in productEnum)
    {
        if ((prod?.Price ?? 0) >= minimumPrice)
        {
            yield return prod;

        }
    }
}

My first question:

decimal arrayTotal = productArray.FilterByPrice(700).TotalPrices();

How the compiler chose the Total Prices method for execute first instead of former method?

My second question: While I debugged the code, I understood TotalPrices method is getting called First, and it goes to below line foreach (Product?prod in products)

After that all of sudden, flow is getting transferred to FilterByPrice method. Then yield will return the value? How?

I want to know how this happened all of a sudden. Is there any internal working I am missing?

Share Improve this question edited Feb 14 at 8:38 Dale K 27.5k15 gold badges58 silver badges83 bronze badges asked Jan 29 at 10:25 stackoverflowresearch456 sstackoverflowresearch456 s 591 silver badge5 bronze badges 0
Add a comment  | 

4 Answers 4

Reset to default 9

How the compiler chose the TotalPrices method for execute first instead of former method?

This is:

  1. Not about extension classes per se
  2. Not completely true in terms what is actually happening (but true in terms of the user code).

The thing is yield return is a syntactic sugar which results in compiler generating special class which encapsulates the user provided logic, exposes it as IEnumerable/IEnumerator and substituting the body of the FilterByPrice with instantiation of that class. I.e FilterByPrice will look something like the following:

[IteratorStateMachine(typeof(<FilterByPrice>d__1))]
[Extension]
[return: Nullable(new byte[] { 1, 2 })]
public static IEnumerable<Product> FilterByPrice([Nullable(new byte[] { 1, 2 })] IEnumerable<Product> productEnum, decimal minimumPrice)
{
    <FilterByPrice>d__1 <FilterByPrice>d__ = new <FilterByPrice>d__1(-2);
    <FilterByPrice>d__.<>3__productEnum = productEnum;
    <FilterByPrice>d__.<>3__minimumPrice = minimumPrice;
    return <FilterByPrice>d__;
}

Full decompilation into C# @sharplab.io

Since the enumeration is lazy here, the user provided code will be executed only when you will start iterating over the generated IEnumerable/IEnumerator.

So FilterByPrice is still called first but your code is not.

You can "overcome" this behavior by moving the yield into local function (can be useful in some fail-fast scenarios):

public static IEnumerable<Product?> FilterByPrice(this IEnumerable<Product?>productEnum, decimal minimumPrice)
{
    return Inner(productEnum, minimumPrice);

    IEnumerable<Product?> Inner(IEnumerable<Product?>productEnum, decimal minimumPrice)
    {
        foreach(Product? prod in productEnum)
        {
            if ((prod?.Price ?? 0) >= minimumPrice)
            {
                yield return prod;
            }
        }
    }
}

Runnable demo @sharplab.io

See also:

  • Execution of an iterator section of the docs

When you call an extension method ExtensionMethod() on SomeType instance:

instance.ExtensionMethod()

what you actually do is ExtensionMethod(instance). This is because extension methods are static methods.

When you chain them

instance
.ExtensionMethod()
.ExtensionMethod2()

You do ExtensionMethod2(ExtensionMethod(instance)).

In your case this means:

decimal arrayTotal = productArray.FilterByPrice(700).TotalPrices();

is really

decimal arrayTotal = TotalPrices(FilterByPrice(productArray,700);

TotalPrices needs an IEnumerable<Product> argument which a call to FilterByPrice should provide. So logically FilterByPrice is called BEFORE TotalPrices.

If FilterByPrice had built the filtered enumerable into a List<Product> and avoided the yield keyword, you will see the debugger step into its body before it steps into TotalPrices.

However, when you use the yield keyword you instruct the compiler to:

  1. Create a hidden class/type that implements IEnumerable/IEnumerator
  2. Take the body of your current method and put it into the IEnumerator.MoveNext method of that new class/type
  3. Rewrite your existing method into essentially:
    IEnumerable<Product> FilterByPrice()
    {
        var ienuemerable = new HiddenEnumerable();
        return ienumerable();
    }
    

Unfortunately, as you have observed, you cannot set/hit ANY breakpoints in this compiler generated method that replaced yours. Personally, I think this is somewhat of a bug especially when setting a break-point at the { opening bracket of the original method.

Instead, the breakpoints that are set will only be hit when the MoveNext method is called. This is what prompted your question. TotalPrices calls the HiddenClass.GetEnumerator().MoveNext when it foreaches, but you think TotalPrices is calling the FilterByPrice method because you are hitting the breakpoints set in its body.

To demonstrate this behavior in a more isolated manner, you can use this code:

var foo = Extensions.FilterByPrice(productArray, 700);
foo.GetEnumerator().MoveNext();

you can't step into the FilterByPrice method from the first line, but only from the second. Then try using List instead of a yield and you would be able to step into from the first line.

For both IEnumerable / yield return and async / await language features, the compiler performs the same transformation of your code.

Your method is moved to a .MoveNext method on a separate class. Local variables are promoted to class fields. Extra return statements are added to pause you code. And a switch statement, similar to Duff's device, is inserted in your code to resume where execution left off.

Collectively these transformations turn your linear code into a state machine. Your code can be paused and resumed, without needing to write such a state machine manually.

Importantly for your example, calling FilterByPrice doesn't start the execution of your method. Instead it creates an instance of this state machine, complete with captured argument variables.

It's only when TotalPrices begins the foreach loop, and calls IEnumerable.MoveNext, that your FilterByPrice method actually starts.

yield may confuse as it returns result of each iteration immediately saving from need to build full result first only to return it.

You could get rid of confusion and make it simpler this same time using Linq. I tested it and it gives this same result. It's my favorite feature in C# I guess.

    public static decimal TotalPrices(this IEnumerable<Product?> products)
    {
        return products.Sum(product => product?.Price ?? 0);
    }

    public static IEnumerable<Product?> FilterByPrice(this IEnumerable<Product?> productEnum, decimal minimumPrice)
    {
        return productEnum.Where(product => product?.Price >= minimumPrice);
    }

本文标签: cUnderstanding the internal working of an extension class functionalityStack Overflow