简体   繁体   中英

How do I add to an ICollection property when iterating object type?

I have need to add to an ICollection<string> property of a class of which I have an IEnumerable of. Here is a complete program which illustrates the problem:

using System;
using System.Collections.Generic;
using System.Linq;

namespace CollectionAddingTest
{
    public class OppDocumentServiceResult
    {
        public OppDocumentServiceResult()
        {
            this.Reasons = new List<string>();
        }

        public Document Document { get; set; }

        public bool CanBeCompleted
        {
            get
            {
                return !Reasons.Any();
            }
        }

        public ICollection<string> Reasons { get; private set; }
    }

    public class Document
    {
        public virtual string Name { get; set; }
    }

    public class Program
    {
        private static void Main(string[] args)
        {
            var docnames = new List<string>(new[] {"test", "test2"});

            var oppDocResult = docnames
                .Select(docName
                        => new OppDocumentServiceResult
                               {
                                   Document = new Document { Name = docName }
                               });

            foreach (var result in oppDocResult)
            {
                result.Document.Name = "works?";
                result.Reasons.Add("does not stick");
                result.Reasons.Add("still does not stick");
            }

            foreach (var result in oppDocResult)
            {
                // doesn't write "works?"
                Console.WriteLine(result.Document.Name);

                foreach (var reason in result.Reasons)
                {
                    // doesn't even get here
                    Console.WriteLine("\t{0}", reason);
                }
            }
        }
    }
}

I would expect that each OppDocumentServiceResult would have its referenced Document.Name property set to works? and each OppDocumentServiceResult should have two reasons added to it. However, neither is happening.

What is special about the Reasons property that I cannot add things to it?

The issue is your initial Select you are instantiating new OppDocumentServiceResult objects. Add a ToList and you should be good to go:

var oppDocResult = docnames
    .Select(docName
            => new OppDocumentServiceResult
                   {
                       Document = new Document { Name = docName }
                   }).ToList();

As Servy pointed out I should have added a bit more detail to my answer, but thankfully the comment he left on Tallmaris' answer takes care of that. In his answer Jon Skeet further expands on the reason, but what it boils down to "is that oppDocResult is the result of a LINQ query, using deferred execution."

The problem is that oppDocResult is the result of a LINQ query, using deferred execution.

In other words, every time you iterate over it, the query executes and new OppDocumentServiceResult objects are created. If you put diagnostics in the OppDocumentServiceResult constructor, you'll see that.

So the OppDocumentServiceResult objects you're iterating at the end are different ones to the ones you've added the reasons to.

Now if you add a ToList() call, then that materializes the query into a "plain" collection (a List<OppDocumentServiceResult> ). Each time you iterate over that list, it will return references to the same objects - so if you add reasons the first time you iterate over them, then you print out the reasons when you iterate over them again, you'll get the results you're looking for.

See this blog post (among many search results for "LINQ deferred execution") for more details.

ForEach() is defined against only List<T> you will not be able to use it to for an ICollection<T> .

You have to options:

((List<string>) Reasons).ForEach(...)

Or

Reasons.ToList().ForEach(...)

Yet, my preferred approach

I would define this extension which can help automating this for you without wasting resources:

public static class ICollectionExtensions
{
    public static void ForEach(this ICollection<T> collection, Action<T> action)
    {
        var list = collection as List<T>;
        if(list==null)
            collection.ToList().ForEach(action);
        else
            list.ForEach(action);
    }
}

Now I can use ForEach() against ICollection<T> .

Fixed like this, converting to List instead of keeping the IEnumerable:

var oppDocResult = docnames
        .Where(docName => !String.IsNullOrEmpty(docName))
        .Select(docName
            => new OppDocumentServiceResult
            {
                Document = docName
            }).ToList();

I can only guess (this is a shot in the dark really!) that the reason behind this is that in an IEnumerable the elements are like "proxies" of the real elements? basically the Enumerable defined by the Linq query is like a "promise" to get all the data, so everytime you iterate you get back the original items? That does not explain why a normal property still sticks...

So, the fix is there but the explanation I am afraid is not... not from me at least :(

只需在类中更改代码

public List<string> Reasons { get; private set; }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM