为何同一列表的两个引用表现如同深拷贝?
在我的架构中,业务对象(简称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实例!
这就解释了为什么:
- 第一次调用
ToList()得到users列表,修改其中UserDto的Contact属性; - 第二次调用
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

