Linq and deferred evaluation - c#

When you use LINQ to define an enumerable collection, either by using the LINQ extension methods or by using query operators,the application
does not actually build the collection at the time that the LINQ
extension method is executed; the collection is enumerated only when
you iterate over it. This means that the data in the original
collection can change between executing a LINQ query and retrieving
the data that the query identifies; you will always fetch the most
up-to-date data.
Microsoft Visual C# 2013 step by step written by John Sharp
I have written the following code:
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 };
IEnumerable<int> res = numbers.FindAll(a => a > 0).Select(b => b).ToList();
numbers.Add(99);
foreach (int item in res)
Console.Write(item + ", ");
The result of the above code is shown bellow:
1, 2, 3, 4, 5,
Why is that going like this? I know about Func, Action and Predicate but I can not figure out what is going on here. Based on the above definition the code is not rational.

Apart from the ToList() at the end, which is creating a new collection, you have another issue.
The problem is that you are not using LINQ at all.
FindAll is not a LINQ extension method.
You should use Where:
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 };
IEnumerable<int> res = numbers.Where(a => a > 0);
numbers.Add(99);
foreach (int item in res)
Console.Write(item + ", ");

First you set an int type list that contains 1,2,3,4,5 .
then you used linq to create and define an enumeration collection .
here describes how the linq work :
first find all numbers that they are bigger than zero, as you see all of the items in the above list are bigger than zero , then select all of them and put them in a list . when you add 99 to the number list it doesn't effect on the enumeration collection that defined because it'll create a new collection and pass the items in it and it hasn't any references to the numbers list .
you can delete .ToList() at the end of the linq expression, it'll result in :
1,2,3,4,5,99 .
Good Luck

ToList creates a new instance of List<T> and copy all the items into it:
http://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,e276d6892241255b
public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source) {
if (source == null) throw Error.ArgumentNull("source");
return new List<TSource>(source);
}
So if you want to have 99 in the res you should add it into res, not into numbers:
...
var res = numbers
.Where(a => a > 0) // Filter out; Select is redundant
.ToList();
res.Add(99);
Console.Write(string.Join(", ", res));

The ToList() is not actually the only problem. FindAll returns a new List. So when you call
IEnumerable<int> res = numbers.FindAll(a => a > 0)
That is the same as doing
IEnumerable<int> newList = new List<int>();
foreach (int old in numbers) {
if (old > 0) newList.Add(old);
}
So when you add a new item to numbers, it is no longer relevant. You are searching against the list returned by FindAll rather than the original list.

You will see the result you expect if you defer (or remove all together) the ToList() operation until your foreach loop. ToList will execute the Linq expression the same as enumerating.
List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 };
IEnumerable<int> res = numbers.FindAll(a => a > 0).Select(b => b);
numbers.Add(99);
foreach (int item in res)
Console.Write(item + ", ");
// another option, but not necessary in most cases...
foreach (int item in res.ToList())
Console.Write(item + ", ");

Related

LINQ fetching sequence values

The below list providing a collection of integers. My requirement is to return sequence values are like first 1,2,3,4,5. once the sequence gets less than or equal 1. fetching will be stopped. I could use for loop to do this operation, but I need to do this using LINQ Extension method. Thanks
If I pass 1, then the result should be like 1,2,3,4,5
If I pass 2, then the result should be like 2,3,4,5
If I pass 3, then the result should be like 3,4,5
List<int> list=new List<int>();
list.Add(1);---------
list.Add(2);---------
list.Add(3);---------
list.Add(4);---------
list.Add(5);---------
list.Add(1);
list.Add(2);
list.Add(3);
var result = list.TakeWhile((item, index) => item > list[0] || index == 0);
You can simply use a loop:
List<int> list2 = new List<int>()
for(int i=0;i<list.Count;i++)
if(i==0 || list[i]>list2[i-1])
list2.Add(list[i]);

Query to get all instead of FirstOrDefault

I need to iterate through all items in a subtree.
Therefore, I want to put all childItems of an predecessor into an array. I tried:
var successors =
TabWebContext.MenuItemSet.Where(m => m.PredecessorId == parentId).ToArray();
I also tried it without .ToArray() and with .ToList().
I have the following loop: while (successors.Count()>0){...} (or rather .Length>0).
The condition is never true.
What do you think am I making wrong? I know there are elements (if I do .FirstOrDefault(), there is at least one element found.
Why do you feel that it needs to be in an array to iterate through it? You could iterate the list like this:
foreach (var successor in TabWebContext.MenuItemSet.Where(m => m.PredecessorId == parentId))
{
// do stuff with 'successor'
}
Just to show an example of how select all from data source works with where clause.
int[] numbers = { 2, 34, 23, 11 }; //data source >> can be EF or ADO.NET
var result = numbers.Where(n => n <= 20).ToList(); // select all with filteration
foreach(int i in result ) //just to loop and
{
Console.WriteLine(i);
}
output :
22
11
so there is no issue can be seen in var successors =
TabWebContext.MenuItemSet.Where(m => m.PredecessorId == parentId).ToArray();
if you do facing an error then show that in your question.

Selecting 3 identical items from list

I need to find if there are 3 identical items in list.
It should compare elements using overridden .Equals() method. I've tried many ways and failed.
It doesn't matter if it returns bool value or items itself.
The function will be called every time after new item is added, so it does not matter how as long as it detects the point when 3 same items are in list.
This is probably something trivial, but my knowledge of Linq is very weak.
Try
return
collection.Any(any => collection.Count(item => item.Equals(any)) == 3);
By grouping items by itself and evaluating if any group contains exactly three items, you will receive expected result.
private bool ContainsTriple<T>(IList<T> items){
return items.GroupBy(i => i).Any(l => l.Count() == 3);
}
To express better my concept:
static class EnumerableExtensions
{
public static IEnumerable<T> FirstRepeatedTimes<T>(this IEnumerable<T> sequence, int threshold)
{
if (!sequence.Any())
throw new ArgumentException("Sequence must contain elements", "sequence");
if (threshold < 2)
throw new ArgumentException("DuplicateCount must be greater than 1", "threshold");
return FirstRepeatedTimesImpl(sequence, threshold);
}
static IEnumerable<T> FirstRepeatedTimesImpl<T>(this IEnumerable<T> sequence, int threshold)
{
var map = new Dictionary<T, int>();
foreach(var e in sequence)
{
if (!map.ContainsKey(e))
map.Add(e, 0);
if (map[e] + 1 == threshold)
{
yield return e;
yield break;
}
map[e] = map[e] + 1;
}
}
}
you would use it like this:
var list = new List<int>() { 1,2,2,3,4,3,3 };
// list contains anything for 3 times?
var found = list.FirstRepeatedTimes(3).Any();
It could potentially consume some more memory, but it enumerates the list at most once. Is this Linq? The way I wrote it, it yields exactly 1 element (the first found), or no element, and you can further compose on top of it if you want. You could use FirstOfDefault() instead of Any(), and have then the found element or 0 (or null if we deal with reference types). This way you have the choice.
It's just another way to see it.
static void Main(string[] args)
{
List<string> col = new List<string>();
col.Add("a");
Console.WriteLine(Has_3(col));
Console.ReadKey();
col.Add("a");
Console.WriteLine(Has_3(col));
Console.ReadKey();
col.Add("a");
Console.WriteLine(Has_3(col));
Console.ReadKey();
col.Add("a");
Console.WriteLine(Has_3(col));
Console.ReadKey();
}
static bool Has_3(List<string> col)
{
return col.Count(x => x == "a").Equals(3);
}
My first thought was that this could probably be done by using the Group() method, something like this:
var ints = new List<int>(new[] { 1, 2, 3, 4, 5, 6, 2, 2 });
var first = ints.GroupBy(n => n)
.Select(g => new { g.Key, Count = g.Count() })
.First(g => g.Count >= 3);
Console.WriteLine("Found {0} instances of {1}", first.Count, first.Key);
This snippet checks for 3 or more of the same item, and selects the first item that meets the criteria, you might want to change this. And adapt it to your specific objects instead of integers.
Here's an extension:
public static bool ContainsNTimes<T>(this IEnumerable<T> sequence, T element, int duplicateCount)
{
if (element == null)
throw new ArgumentNullException("element");
if (!sequence.Any())
throw new ArgumentException("Sequence must contain elements", "sequence");
if (duplicateCount < 1)
throw new ArgumentException("DuplicateCount must be greater 0", "duplicateCount");
bool containsNTimes = sequence.Where(i => i.Equals(element))
.Take(duplicateCount)
.Count() == duplicateCount;
return containsNTimes;
}
Usage:
var list = new List<int>() { 1,2,2,3,4,3,3 };
// list contains 2 for 3 times?
bool contains2ThreeTimes = list.ContainsNTimes(2, 3);
// any element in the list iscontained 3 times (or more)?
bool anyContains3Times = list.Any(i => list.ContainsNTimes(i, 3));
Console.WriteLine("List contains 2 for 3 times? " + contains2ThreeTimes); // false
Console.WriteLine("Any element in the list is contained 3 times (or more)? " + anyContains3Times); // true (3)
Demo: http://ideone.com/Ozk9v
Should be quite efficient since it uses deferred execution. It enumerates the sequences until n-items were found.

Linq TakeWhile depending on sum (or aggregate) of elements

I have a list of elements and want to takeWhile the sum (or any aggregation of the elements) satisfy a certain condition. The following code does the job, but i am pretty sure this is not an unusual problem for which a proper pattern should exist.
var list = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
int tmp = 0;
var listWithSum = from x in list
let sum = tmp+=x
select new {x, sum};
int MAX = 10;
var result = from x in listWithSum
where x.sum < MAX
select x.x;
Does somebody know how to solve the task in nicer way, probably combining TakeWhile and Aggregate into one query?
Thx
It seems to me that you want something like the Scan method from Reactive Extensions (the System.Interactive part) - it's like Aggregate, but yields a sequence instead of a single result. You could then do:
var listWithSum = list.Scan(new { Value = 0, Sum = 0 },
(current, next) => new { Value = next,
Sum = current.Sum + next });
var result = listWithSum.TakeWhile(x => x.Sum < MaxTotal)
.Select(x => x.Value);
(MoreLINQ has a similar operator, btw - but currently it doesn't support the idea of the accumulator and input sequence not being the same type.)

LINQ Keyword search with orderby relevance based on count (LINQ to SQL)

This is what i have now as a very basic search:
var Results = from p in dx.Listings select p;
if (CategoryId > 0) Results = Results.Where(p => p.CategoryId == CategoryId);
if (SuburbId > 0) Results = Results.Where(p => p.SuburbId == SuburbId);
var OrderedResults = Results.OrderByDescending(p => p.ListingType);
OrderedResults = OrderedResults.ThenByDescending(p => p.Created);
I understand that i can add in a .Contains() or similar and put in keywords from a keyword box (split into individual items) and that should get the list of results.
However i need to order the results by basic relevance. Meaning that if record A contains 2 of the keywords (in the 'Body' nvarchar(MAX) field) it should be higher than record B that only matches against 1 of the keywords. I don't need a full count of every hit... however if thats eaiser to manage that would be fine.
So is there any way to get the hit count directly in as part of the orderby nicely? I can manage it by getting the results and parsing however i really don't want to do that as parsing possibly thousands could chug the IIS machine, while the SQL Server is a decently powerful cluster :)
If anyone has any ideas or tips it would be a big help.
If I understand you correctly you want to call OrderyByDescending( p => p.Body ) but it should be ordered by how many times a certain word appreas in p.Body ?
Then you should be able to create a method that counts the occurrences and returns the count number then you can simply do OrderyByDescending( p => CountOccurences(p.Body) )
You can alternatively create a BodyComparer class that implements IComparer and then pass it to OrderByDescending
EDIT:
take a look a this link Enable Full Text Searching
Here is a simple example, if I understand what you're looking for correctly:
var storedData = new[]{
new int[] {1, 2, 3, 4},
new int[] {1, 2, 3, 4, 5}
};
var itemsFromTextBox = new[] { 3, 4, 5 };
var query = storedData.Where(a => a.ContainsAny(itemsFromTextBox))
.OrderByDescending(a => itemsFromTextBox.Sum(i => a.Contains(i)? 1:0));
With the following ContainsAny extension:
public static bool ContainsAny<T>(this IEnumerable<T> e1, IEnumerable<T> e2)
{
foreach (var item in e2)
{
if (e1.Contains(item)) return true;
}
return false;
}

Categories

Resources