探究IEnumerable<>的本质(少有的清晰易懂)

一、背景

在项目开发过程中有个方法的参数被定义成IEnumerable<AttStatistics>类型,方法内部会遍历列表并对元素的某些属性赋值,赋值操作执行完毕后我再使用该参数进行其他后续操作时,发现通过遍历赋值的属性全都为空,由此引发了对IEnumerable<T>变量的探索。

二、探究IEnumerable<>的本质

1、Linq.Where<TSource>()

public static async Task<int> Main(string[] args)
{
    try
    {
        IHost host = CreateDefaultBuilder().Build();
        IServiceProvider serviceProvider = host.Services;

        List<AttStatistics> attStatistics = new List<AttStatistics>()
            {
                new AttStatistics { HappyPathId="HappyPathId01", AttChecksum="AttChecksum01", Count=11},
                new AttStatistics { HappyPathId="HappyPathId02", AttChecksum="AttChecksum02", Count=21},
            };
        IEnumerable<AttStatistics> ienum = attStatistics.Where(item => item.Count > 1);
        foreach (AttStatistics att in ienum)
        {
            att.CreatedBy = "newUser";
        }

        return 0;
    }
    catch (Exception ex)
    {
        return 1;
    }
}

Linq.Where<TSource>()的返回值是attStatistics集合中的元素,ienum和attStatistics中的元素实际上是同一个对象,AttStatistics是引用类型的,所以修改ienum中的元素时attStatistics中的元素也会修改。

2、Linq.Select<TSource, TResult>()返回值与被遍历的元素有关

//为了方便大家分享工作心得、交流技术问题,我创建了QQ群389591879,
//大家也可以在里边相互了解各自公司的信息,希望能对大家有所帮助,同道中人欢迎加群
public static async Task<int> Main(string[] args)
{
    try
    {
        IHost host = CreateDefaultBuilder().Build();
        IServiceProvider serviceProvider = host.Services;

        List<AttStatistics> attStatistics = new List<AttStatistics>()
        {
            new AttStatistics { HappyPathId="HappyPathId01", AttChecksum="AttChecksum01", Count=11},
            new AttStatistics { HappyPathId="HappyPathId02", AttChecksum="AttChecksum02", Count=21},
        };
        IEnumerable<AttStatistics> ienum = attStatistics.Select(item =>
        {
            Console.WriteLine("Start->" + JsonSerializer.Serialize(item));
            item.Count++;
            Console.WriteLine("End->" + JsonSerializer.Serialize(item) + "\r\n");
            return item;
        });
        Console.WriteLine("attStatistics01->" + JsonSerializer.Serialize(attStatistics) + "\r\n");
        foreach (AttStatistics att in ienum)
        {
            att.CreatedBy = "newUser";
        }
        Console.WriteLine("ienum01->" + JsonSerializer.Serialize(ienum) + "\r\n");

        Console.WriteLine("ienum02->" + JsonSerializer.Serialize(ienum) + "\r\n");

        var ienumItem01 = ienum.ElementAt(1);
        Console.WriteLine("ienumItem01->" + JsonSerializer.Serialize(ienumItem01) + "\r\n");

        var ienumList = ienum.ToList();
        Console.WriteLine("ienumList->" + JsonSerializer.Serialize(ienumList) + "\r\n");

        Console.WriteLine("attStatistics02->" + JsonSerializer.Serialize(attStatistics) + "\r\n");

        return 0;
    }
    catch (Exception ex)
    {
        return 1;
    }
}

Linq.Select<TSource, TResult>()的返回值可以是attStatistics集合中的元素也可以是新元素,当返回值与attStatistics集合中的元素有关时,ienum和attStatistics中元素的关系可以参考第一部分。

与第一部分不同的是我在Linq.Select<TSource, TResult>()中加了日志,这一关键信息能帮我们搞清楚IEnumerable<>对象到底是什么!当Linq.Select<TSource, TResult>()的代码执行完毕,程序已经走到下面的代码了,但没有使用IEnumerable<>对象中的元素时,实际上构造IEnumerable<>对象的逻辑并没有真正执行。因为Linq.Select<TSource, TResult>()接收的是委托类型参数,要到真正使用IEnumerable<>对象中的元素时委托才会执行。

3、Linq.Select<TSource, TResult>()返回与被遍历元素无关的新元素

public static async Task<int> Main(string[] args)
{
    try
    {
        IHost host = CreateDefaultBuilder().Build();
        IServiceProvider serviceProvider = host.Services;

        List<AttStatistics> attStatistics = new List<AttStatistics>()
        {
            new AttStatistics { HappyPathId="HappyPathId01", AttChecksum="AttChecksum01", Count=11},
            new AttStatistics { HappyPathId="HappyPathId02", AttChecksum="AttChecksum02", Count=21},
        };
        IEnumerable<AttStatistics> ienum = attStatistics.Select(item =>
        {
            Console.WriteLine("Start->" + JsonSerializer.Serialize(item));

            AttStatistics newItem = new AttStatistics()
            {
                HappyPathId = item.HappyPathId,
                AttChecksum = item.AttChecksum, 
                Count = item.Count + 1
            };
            Console.WriteLine("End->" + JsonSerializer.Serialize(item) + "\r\n");
            return newItem;
        });
        Console.WriteLine("attStatistics01->" + JsonSerializer.Serialize(attStatistics) + "\r\n");
        foreach (AttStatistics att in ienum)
        {
            att.CreatedBy = "newUser";
        }
        Console.WriteLine("ienum01->" + JsonSerializer.Serialize(ienum) + "\r\n");

        Console.WriteLine("ienum02->" + JsonSerializer.Serialize(ienum) + "\r\n");

        Console.WriteLine("attStatistics02->" + JsonSerializer.Serialize(attStatistics) + "\r\n");

        return 0;
    }
    catch (Exception ex)
    {
        return 1;
    }
}

当Linq.Select<TSource, TResult>()的返回值是与attStatistics集合中的元素无关的新元素时,我们先遍历ienum对其中的属性赋值,赋值操作执行完毕后我再使用该参数进行其他后续操作时,还会再针对attStatistics执行一遍Linq.Select<TSource, TResult>()的委托逻辑,每次使用ienum都会重新生成新的对象,所以遍历对属性赋值在这种情况下是无效的。

三、学会查看变量的内存地址

问:为什么这里要教人如何查看变量的内存地址?

答:我最初不是通过在attStatistics.Select()中加日志的方式来定位问题的,我是在foreach()之前先拿到ienum.ElementAt(0),然后在foreach()内部打断点拿到第一个元素,然后查看这两个对象的地址发现不一样,这样就能确定IEnumerable<T>和List<T>是不完全一样的,由此推断出有可能IEnumerable<T>中的元素在每次获取时都不同。

以前在VS2017环境上写过如何查看变量的内存地址,但是VS2022与VS2017有些不同,所以研究了一下又整理出来,希望对大家有所帮助。

VS2017文章地址:C#中字符串的内存分配与暂存池【非常详细】,感兴趣可以取看看。

四、总结

整理这篇文章也是花了不少的心思,如果本文对你有帮助,请点赞、评论支持下,谢谢各位。

为了方便大家分享工作心得、交流技术问题,我创建了QQ群389591879,
大家也可以在里边相互了解各自公司的信息,希望能对大家有所帮助,同道中人欢迎加群。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

changuncle

若恰好帮到您,请随心打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值