You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何同一列表的两个引用表现如同深拷贝?

问题描述

在我的架构中,业务对象(简称BO)返回OperationResult<泛型类型>标准结果,每个BO结果都会附带监控信息(操作成功/失败状态、异常、操作别名、错误码等)。所有BO调用都通过名为manager的对象中介,它负责将BO结果包装为OperationResult。

项目中不使用延迟加载或延迟执行,所有返回类型在返回时都是就绪状态。

基于上述前提,我遇到了一个异常行为:两个本该指向相同元素的列表,表现却完全独立(注释中有详细说明):

var opResult = manager.Execute(userBo.FindUser, token, query);
// userBo.FindUser返回的是自定义分页类型的数据
// 每页内容的类型是IEnumerable而非List

if (opResult.Success && opResult.ReturnData != null && opResult.ReturnData.PageContent != null)
{
    request.ItemCountAfterProcessing = opResult.ReturnData.ItemsCount;
    request.ItemCountInPage = opResult.ReturnData.ActualItemsPerPage;

    var users = opResult.ReturnData.PageContent.ToList();
    // 这里把分页内容转成List,注意数据源本身已经是List,只是自定义的BasePageResults类型把它声明为IEnumerable<T>
    // 接下来会给users列表中的用户附加联系信息,这一步执行正常,每个用户都能正确关联到联系信息

    var usersIds = users.Select(usr => usr.Id).ToList();

    var contactQuery = new PagedQueryDto<tbl_usr_Contact> ( addr => usersIds.Contains(addr.USER_ID) );

    var opContactFetchResult = manager.Execute(userBo.FindAddressBook, token, contactQuery);
    if (opContactFetchResult.Success && opContactFetchResult.ReturnData != null && opContactFetchResult.ReturnData.PageContent != null)
    { 
        Dictionary<int, ContactDto> indexedContacts = opContactFetchResult.ReturnData.GroupBy ( addr => addr.UserId )
                                                                                     .ToDictionary ( group => group.Key , group => group.FirstOrDefault() );

        foreach (var user in users)
            if (indexedContacts.ContainsKey(user.Id))
                user.Contact = indexedContacts[user.Id];
    }

    var newListWithSameReference = opResult.ReturnData.PageContent.ToList();
    // 此时查看users列表,每个用户都已附加联系信息
    // 但查看newListWithSameReference时,用户却处于初始状态(没有联系信息)
    // 我无法理解的点在于:两个变量都来自opResult.ReturnData.PageContent,按道理应该指向相同的元素引用
    // userBo.FindUser返回的分页列表中,PageContent实际是List<T>,只是因为BasePageResults的签名声明为IEnumerable<T>

    result = opResult.ReturnData.PageContent.ToList().Select ( usr => new DestinationUserDto ( usr) ).ToList();
}

return result;

为了明确类型定义,附上自定义分页列表类型和FindUser方法:

自定义分页列表定义

public class BasePageResults<TEntity> : IEnumerable<TEntity> where TEntity : new()
{
    public TEntity this[int index] 
    { 
        get
        {
            if (index >= 0 && index < (this.PageContent?.Count() ?? 0))
                this.PageContent.ElementAt(index);

            throw new IndexOutOfRangeException();
        }

        set
        {
            if (index >= 0 && index < (this.PageContent?.Count() ?? 0))
            {
                var temporaryList = new List<TEntity>(this.PageContent);
                temporaryList[index] = value;

                this.PageContent = temporaryList;
            }

            throw new IndexOutOfRangeException();
        }
    }

    /// <summary>
    /// 当前查询页的内容
    /// </summary>
    public IEnumerable<TEntity> PageContent { get; set; }

    /// <summary>
    /// 当前页码
    /// </summary>
    public int PageNumber { get; set; }

    /// <summary>
    /// 每页应包含的条目数
    /// </summary>
    public int ItemsPerPage { get; set; }

    /// <summary>
    /// 当前页实际包含的条目数
    /// </summary>
    public int ActualItemsPerPage { get { return this.PageContent?.Count() ?? 0; } }

    /// <summary>
    /// 符合查询条件的总条目数(不受当前页条目数限制)
    /// </summary>
    public long ItemsCount { get; set; }

    /// <summary>
    /// 总页数
    /// </summary>
    public int PagesCount { get { return this.ItemsPerPage <= 0 ? 0 : (int)Math.Ceiling((double)this.ItemsCount / (double)this.ItemsPerPage ); } }

    public IEnumerator<TEntity> GetEnumerator()
    {
        return this.PageContent?.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.PageContent?.GetEnumerator();
    } 
}

FindUser方法结构

/// <summary>
/// 对用户仓库执行查询,找到对应的UserDto,结果以分页形式返回
/// </summary>
/// <param name="query">应用到数据源的查询条件</param>
/// <returns>查询到的用户分页数据</returns>
[PermissionRequired(PermissionAttribute.Login | PermissionAttribute.Read)]
[Intent(IntentDescription.Read)]
public BasePageResults<UserDto> FindUser(PagedQueryDto<tbl_usr_User> query)
{
    if (query == null)
        throw new ExtendedArgumentException("query");

    using (var context = ServiceLocator.ConnectionProvider.Instace<UserRoleDataContext>())
    {
        var repository = new UserRepository(context);
        var dbQuery = repository.Read(query.Query);

        var page = base.GenericPagedRead(dbQuery, query);

        return new BasePageResults<UserDto> ()
        {
            ItemsCount   = page?.ItemsCount    ?? 0,
            ItemsPerPage = page?.ItemsPerPage  ?? 0,
            PageNumber   = page?.PageNumber    ?? 0,
            PageContent  = page?.PageContent?.Select ( usr => (new UserDto()).Feed(usr) ) ?? new List<UserDto> () 
           // page?.PageContent是ORM返回的List,之后会映射为DTO,所以这里肯定是List,不存在延迟加载,ORM在这一步已经完成数据加载
        };
    }
}

我完全无法理解为何users和newListWithSameReference表现得像是深拷贝的结果——按道理它们应该是同一元素的浅拷贝,修改users列表中元素的属性,newListWithSameReference中对应的元素属性也应该同步变化。


问题原因及解决方案

核心原因:PageContent的Select是延迟执行的!

你在FindUser方法中给PageContent赋值时用了Select:

PageContent = page?.PageContent?.Select ( usr => (new UserDto()).Feed(usr) ) ?? new List<UserDto> ()

虽然你注释说不存在延迟加载,但这里的Select返回的是IEnumerable<T>,它的执行是延迟的——每次你遍历它(包括调用ToList())时,都会重新执行Select中的逻辑,也就是每次都会创建新的UserDto实例!

这就解释了为什么:

  1. 第一次调用ToList()得到users列表,修改其中UserDto的Contact属性;
  2. 第二次调用ToList()得到newListWithSameReference时,Select会重新执行,生成全新的UserDto实例,这些实例没有被修改过,自然没有联系信息。

解决方案

在FindUser方法中,把Select的结果立即转为List,避免延迟执行:

PageContent = page?.PageContent?.Select ( usr => (new UserDto()).Feed(usr) ).ToList() ?? new List<UserDto> ()

这样PageContent就会是一个固定的List<UserDto>实例,每次调用ToList()都会基于这个实例创建新的List(但元素引用都是同一个UserDto对象),修改其中元素的属性后,后续的ToList()得到的列表元素也会同步变化。

另外,检查你的BasePageResults类的索引器set方法,每次设置元素时都会创建新的List并替换PageContent,不过这不是当前问题的原因,但如果后续有修改元素的场景,需要注意这个行为。


内容的提问来源于stack exchange,提问作者Skary

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 06:40:35