I have some data stored in a dictionary where the values are basically a list of objects with few attributes in them. Right now I'm looping through as following to get the data stored in a specific attribute. These data are then added into a drop down list (unity UI dropdown)
foreach (KeyValuePair<string, List<NameIDValuePair>> kvp in TeamValuePair)
{
List<NameIDValuePair> list = kvp.Value;
if(kvp.Key == teamNames.options[teamNames.value].text)
{
foreach (var rec in list)
{
screenNamesDropDown.options.Add(new TMP_Dropdown.OptionData { text = rec.ScreenName });
}
}
}
teamNames and screenNamesDropDown are dropdown elements part of my unity UI.
The structure of the NameIdValuePair looks as follows:
public class NameIdValuePair
{
public string ScreenName { get; private set; }
public string ScreenId { get; private set; }
}
I would like to optimize this piece of code in a better way using linq - so that it's a bit more readable. Since I'm pretty new to linq, i'm not really sure if I'm using the right keywords when searching for suggestions but so far I haven't had much success in finding any helpful suggestion.
Thanks
As mentioned before instead of looping a Dictionary - where we already know that the keys are unique - you could simply use Dictionary.TryGetValue
// do this only once!
var key = teamNames.options[teamNames.value].text;
if (TeamValuePair.TryGetValue(key, out var list))
{
foreach(var item in list)
{
screenNamesDropDown.options.Add(new TMP_Dropdown.OptionData(item.ScreenName));
}
}
and then actually the only place where you could use Linq if you really want to would maybe be in
var key = teamNames.options[teamNames.value].text;
if (TeamValuePair.TryGetValue(key, out var list))
{
screenNamesDropDown.options.AddRange(list.Select(item => new TMP_Dropdown.OptionData(item.ScreenName)));
}
if this makes it better to read is questionable though.
And in general the question would also be if you always want to Add (AddRange) to the screenNamesDropDown.options or if you maybe want to actually replace the options. Then instead of AddRange you could do
screenNamesDropDown.options = list.Select(item => new TMP_Dropdown.OptionData(item.ScreenName)).ToList();
Related
Not sure I understand why I can do this with a for loop and not a foreach loop?
This is the code that works. Looping through a BindingList Products, finding a match and then assigning that product at index i to the new product that's passed in.
public static void UpdateProduct(int productToUpdateID, Product productToUpdate)
{
for (int i = 0; i < Products.Count; i++)
{
if (Products[i].ProductID == productToUpdateID)
{
Products[i] = productToUpdate;
}
}
}
If I try to do this with a foreach loop I get an error that I cannot assign to the iterator variable. What is the reasoning for this and is there a way to get around it or is using a for loop for this kind of problem the best solution?
This is essentially what I'm trying to do.
public static void UpdateProduct(int productToUpdateID, Product productToUpdate)
{
foreach(Product product in Products)
{
if (product.ProductID == productToUpdateID)
{
product = productToUpdate;
}
}
}
I can do something like this and reassign all the properties explicitly but want to see if there is another way to do it.
foreach(Product product in Products)
{
if (product.ProductID == productToUpdateID)
{
product.Name = productToUpdate.Name;
}
}
Thanks!
The foreach construct is for when you want to do something with each item in the list. That does not seem to be what you are doing. You are modifying the list itself, by removing an item and replacing it.
Personally I would not use a loop at all, I'd just remove the old item and add the new one.
public static void UpdateProduct(int productToUpdateID, Product productToUpdate)
{
Products.RemoveAll( x => x.ProductID == productToUpdateID );
Products.Add( productToUpdate );
}
Or if you wish to preserve order:
public static void UpdateProduct(int productToUpdateID, Product productToUpdate)
{
var index = Products.FindIndex( x => x.ProductID == productToUpdateID );
Products[index] = productToUpdate;
}
The reasons have already been given, but as a minor detail: this is sometimes possible; there is an alternative syntax in recent C# that uses a ref-local for the iterator value:
foreach (ref [readonly] SomeType value in source)
which is only available for some scenarios - naked arrays, spans, or custom iterator types with a ref-return Current - and as long as the optional readonly modifier is not used, you can assign directly via the value variable, since this is a direct reference to the underlying source. The uses for this are rare and niche. If Products is a List<T>, you could combine this with CollectionMarshal.AsSpan(...) to achieve what you want, but frankly I'd consider that hacky (apart from other things, it would bypass the list's internal change protections). Basically: don't do this, but : it isn't entirely impossible.
The foreach loop iterates over the elements of a collection, and the iteration variable is simply a reference to the current element in the collection.
The reason you cannot modify the iteration variable itself is that it is a read-only reference to the element in the collection. Modifying the iteration variable would not change the element in the collection; it would only change the reference.
Alternative ways are already mentioned in the above answers.
Just for the record. IMHO the best way is to use a foreach loop with a modified code like this. It only makes one iteration
int i=-1;
foreach (var product in products )
{
i++;
if (product.ProductID == productToUpdate.ProductID)
{
products[i]=productToUpdate;
break;
}
}
But if you want to use linq for some reason, you can do it in one line
products = products.Select(x => x = x.ProductID == productToUpdate.ProductID?productToUpdate:x).ToList();
I have a List of dynamic objects that I am trying to use dynamic Linq on. I am using dynamic objects because I do not know the properties that will be coming into the object. Linq works on my dynamic object, but, to avoid giant hard coding if statements, I would like to use dynamic Linq to search my list. The top half of the code snippet works but I need it to work dynamically so I can create a query string from my properties and filter that way.
public List<dynamic> GetFilteredLocationData(List<dynamic> locationData, string searchTerm){
//Does work
List<dynamic> totalResults = locationData.Where(x => x.Street.ToLower().Contains(searchTerm.ToLower()) ||
x.Street.ToLower().Contains(searchTerm.ToLower()) ||
x.Zip.ToLower().Contains(searchTerm.ToLower()));
//Does not work
var testQueryString = "(Street == \"king\")";
var testResult = locationData.Where(testQueryString);
return totalResults;
}
The runtime error I receive: No property or field 'Street' exists in type 'Object'
That error makes sense as object by default doesn't contain 'Street' but I'd expect the dynamic Linq to behave like the code above it. Is there something I am doing wrong here, or should I take a different approach? I can provide more detail if needed.
Thanks in advance!
Finally I got a working solution! It may not be the most efficient but it works for my needs and allows me to keep the dynamic nature I was hoping to retain. The solution was to drop Linq entirely and use a good old for-each loop. The Important part was the IDictionary which allowed me to search each row for the key value pair. This is the same functionality I was going for, just ditched linq.
public List<dynamic> GetFilteredLocationData(List<dynamic> locationData, string searchTerm){
List<dynamic> totalResults = new List<dynamic>();
List<string> locationProperties = new List<string> {"dynamic properties here, this was filled by call to DB for info pertaining to certain location combined with unique data"}
foreach (var locData in locationData)
{
var currentLoc = locData;
var currentLocDict = (IDictionary<string, object>)currentLoc;
bool containsSearchTerm = CheckIfLocationContainsSearch(currentLocDict, allLocationProperties, searchTerm);
if (containsSearchTerm)
{
totalResults.Add(locData);
}
}
}
public bool CheckIfLocationContainsSearch(IDictionary<string,object> location, List<string> locationProperties, string searchTerm){
foreach (var locProp in locationProperties)
{
if (location[locProp].ToString().ToLower().Contains(searchTerm))
{
return true;
}
}
return false;
}
When running the following code I receive an exception "CypherTypeException: Collections containing mixed types can not be stored in properties." - What am I doing wrong, and what needs to change to make it work?
var wordObjs = new List<object>();
foreach (string word in WordGroups.GetAllWords())
{
wordObjs.Add(new { Value = word});
}
GraphClient.Cypher
.Create("(word:Word {words})")
.WithParam("words", new { words = wordObjs})
.ExecuteWithoutResults();
One solution would be use a concrete class
private class Value {
public string Value {get;set;}
}
and use new List<Value>() instead, I think the client is having trouble with the anonymous nature of your List.
How to make the following code shorter, perhaps using anonymous method or extensions and LINQ.
Since I have to repeat this code several times and I want to make it as succinct as possible.
var imagesToUnlock = App.ImageListVM.Items.Where(img => img.Category == key);
foreach (var image in imagesToUnlock)
{
image.IsLocked = false;
}
The other solutions here feel dirty because they mutate objects in a collection via the use of LINQ.
I would instead, put the code and the filter condition into an extension method and call that:
public static IEnumerable<Item> UnlockWhere(this IEnumerable<Item> list, Func<Item, bool> condition) {
foreach (var image in list)
if (condition(image)) {
image.IsLocked = false;
yield return image;
}
}
The keeps the immutability-concerns of LINQ intact and still produces the expected result.
The call becomes:
var unlockedItems = App.ImageListVM.Items.UnlockWhere(img => img.Category == key);
EDIT
Re-written to completely remove LINQ. Instead, this new method iterates only once and returns a new, mutated collection.
Not the most efficient way to do it, but I believe you can do
var imagesToUnlock = App.ImageListVM.Items.Where(img => img.Category == key).ToList().Foreach(f => f.IsLocked = false);
Check out the Foreach method on List<T> for more info.
I would also like to note (as some have pointed out in the comments) that this is not considered best practice by some people. You should take a look at this article by Eric Lippert, who explains the issue in better detail.
Here's a stab as an extension method
Code
public static IEnumerable<T> SetPropertyValues<T>(this IEnumerable<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
yield return item;
}
}
Usage
private class Foo
{
public string Bar { get; set; }
}
[TestMethod]
public void SetPropertyValuesForMiscTests()
{
var foos = new[] { new Foo { Bar = "hi" }, new Foo { Bar = "hello" } };
var newList = foos.SetPropertyValues(f => f.Bar = "bye");
Assert.AreEqual("bye", newList.ElementAt(0).Bar);
Assert.AreEqual("bye", newList.ElementAt(1).Bar);
}
I tested it and it works fine.
Yeah you can do this. Adapted from this answer.
imagesToUnlock.Select(i => {i.IsLocked = false; return i;}).ToList();
Edit: A lot of people are saying this is bad practice. I agree with dasblinkenlight here.. Exploring the limits of LINQ and C# is our duty as programmers. It isn't unreasonable to change the objects type from the DTO to the view model or domain object, I know its not the best, but if encapsulated and commented it isn't the end of the world to use select to do this. But please be conscious of the best practices explained by Eric.
I have the following method that takes an extremely long time to run and would love some help to make it run faster and or be more efficient.
The main responsibility of the method is to take a list of data points created from a CSV file, map the Name property of the file datapoints to the to the HistorianTagname property in a list of tagnames by the DataLoggerTagname property and create a resulting list from the mapping. If the mapping does not exist, the file datapoint is ignored.
I know it that was long-winded, but I hope it makes sense. It may be easier just to look at the method:
private IEnumerable<DataPoint> GetHistorianDatapoints(IEnumerable<DataPoint> fileDatapoints, IEnumerable<Tagname> historianTagnames)
{
/**
** REFACTOR THIS
**/
foreach (var fileDatapoint in fileDatapoints)
{
var historianTagname = historianTagnames.FirstOrDefault(x => x.DataLoggerTagname.Equals(fileDatapoint.Name, StringComparison.OrdinalIgnoreCase));
if (historianTagname != null)
{
var historianDatapoint = new DataPoint();
historianDatapoint.Name = historianTagname.HistorianTagname;
historianDatapoint.Date = fileDatapoint.Date;
historianDatapoint.Value = fileDatapoint.Value;
yield return historianDatapoint;
}
}
}
Notes:
I have complete control of classes and methods of mapping, so if I am doing something fundamentally wrong. I would love to know!
Thanks!
I would start by fixing up:
var historianTagname = historianTagnames.FirstOrDefault(x => x.DataLoggerTagname.Equals(fileDatapoint.Name, StringComparison.OrdinalIgnoreCase))
That's a pretty expensive operation to run every iteration through this loop.
Below is my proposition:
private IEnumerable<DataPoint> GetHistorianDatapoints(IEnumerable<DataPoint> fileDatapoints, IEnumerable<Tagname> historianTagnames)
{
var tagNameDictionary = historianTagnames.ToDictionary(t => t.DataLoggerTagname, StringComparer.OrdinalIgnoreCase);
foreach (var fileDatapoint in fileDatapoints)
{
if (tagNameDictionary.ContainsKey(fileDatapoint.Name))
{
var historianTagname = tagNameDictionary[fileDatapoint.Name];
var historianDatapoint = new DataPoint();
historianDatapoint.Name = historianTagname.HistorianTagname;
historianDatapoint.Date = fileDatapoint.Date;
historianDatapoint.Value = fileDatapoint.Value;
yield return historianDatapoint;
}
}
}
Like #Sheldon Warkentin said FirstOrDefault is probably bottle neck of your function, i s better to create historianTagnames a Dictionary where Name is key, then in your function you can get value by key.
Something like bellow:
// this is passed to method
IDictionary<string, Tagname> historianTagnames;
// .. method body
var historianTagname = historianTagnames[fileDatapoint.Name];
ofcourse you need to add proper if's.
As others have said, a Dictionary<string, Tagname> might perform better.
var historianDict = new Dictionary<string, Tagname>();
foreach (var tagName in historianTagnames) {
historianDict[tagName.DataLoggerTagname.ToLowerInvariant()] = tagName;
}
foreach (var fileDatapoint in fileDatapoints) {
if (historianDict.ContainsKey(fileDatapoint.Name.ToLowerInvariant()) {
// ...
}
}