Union of two objects based on the equality of their members

Let's start by defining a class for an example:

public class Person { public string FirstName; public string LastName; public int Age; public int Grade; } 

Now suppose I have a List<Person> called people containing 3 objects:

 {"Robby", "Goki", 12, 8} {"Bobby", "Goki", 10, 8} {"Sobby", "Goki", 10, 8} 

What I'm looking for is a way to get the following single Person object:

 {null, "Goki", -1, 8} 

where fields that are the same in all objects retain their value, and fields that have several values ​​are replaced by some invalid value.

My first thought consisted of:

 Person unionMan = new Person(); if (people.Select(p => p.FirstName).Distinct().Count() == 1) unionMan.FirstName = people[0].FirstName; if (people.Select(p => p.LastName).Distinct().Count() == 1) unionMan.LastName = people[0].LastName; if (people.Select(p => p.Age).Distinct().Count() == 1) unionMan.Age = people[0].Age; if (people.Select(p => p.Grade).Distinct().Count() == 1) unionMan.Grade = people[0].Grade; 

Unfortunately, a real business object has many more members than four, and it is tiring to write and suppress, so that someone else can see for the first time.

I also thought about how to use reflection to put these repeating checks and assignments in a loop:

 string[] members = new string[] { "FirstName", "LastName", "Age", "Grade" }; foreach (string member in members) { if (people.Select(p => p.**member**).Distinct().Count() == 1) unionMan.**member** = people[0].**member**; } 

where ** member ** will be, however, reflection will allow you to get and save this particular element (if possible).

While the first solution will work, and the second that I assume will work, does anyone have a better alternative solution to this problem? If not, is reflection possible as described above possible?

+6
source share
3 answers

It is inefficient to make values ​​different from all only for counting individual members. You have a shortcut script in which searching for a single value in any of the subsequent elements that does not have the same value as the first element of the element means that you have an invalid state for this column.

Something like this should work, although more work is required, if any of the members is an array, you need a recursive evaluation or other more complex logic (note that I have not tested this):

 public static T UnionCombine<T>(this IEnumerable<T> values) where T : new() { var newItem = new T(); var properties = typeof(T).GetProperties(); for (var prop in properties) { var pValueFirst = prop.GetValue(values.First(), null); var useDefaultValue = values.Skip(1).Any(v=>!(Object.Equals(pValueFirst, prop.GetValue(v, null)))); if (!useDefaultValue) prop.SetValue(newItem, pValueFirst, null); } return newItem; } 
+5
source

Your last idea seems good to me, something like this:

 List<Person> persons = new List<Person>() { new Person(){ FirstName="Robby", LastName="Goki", Age=12, Grade=8}, new Person(){ FirstName="Bobby", LastName="Goki", Age=10, Grade=8}, new Person(){ FirstName="Sobby", LastName="Goki", Age=10, Grade=8}, }; var properties = typeof(Person).GetProperties(); var unionMan = new Person(); foreach (var propertyInfo in properties) { var values = persons.Select(x => propertyInfo.GetValue(x, null)).Distinct(); if (values.Count() == 1) propertyInfo.SetValue(unionMan, propertyInfo.GetValue(persons.First(), null), null); } 

A few observations:

  • your class members should be defined as properties, not public members, and both get and set accessor should be public.
  • the default constructor should specify "invalid" values ​​(as @ RaphaëlAlthaus correctly suggested)

Thus, the Person class will look like this:

 public class Person { public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } public int Grade { get; set; } public Person() { this.FirstName = null; this.LastName = null; this.Age = -1; this.Grade = -1; } } 
+2
source

Update:. Since you do not have control over the Person class, and the state is defined in public fields, not in properties, I updated the solution to solve this problem.

I would recommend using reflection. You would like to get FieldInfo (or PropertyInfo ) ahead of time, rather than getting it for every record in your LINQ query. You can get them using Type.GetField and Type.GetProperty . When you have this, you can simply use FieldInfo / PropertyInfo.GetValue and FieldInfo / PropertyInfo.SetValue .

For instance:

 Type personType = typeof(Person); foreach(string member in members) { // Get Fields via Reflection FieldInfo field = peopleType.GetField(member); if(field != null) { if (people.Select(p => field.GetValue(p, null) ).Distinct().Count() == 1) { field.SetValue(unionMan, field.GetValue(people[0], null), null); } } else // If member is not a field, check if it a property instead { // Get Properties via Reflection PropertyInfo prop = peopleType.GetProperty(member); if(prop != null) { if (people.Select(p => prop.GetValue(p, null) ).Distinct().Count() == 1) { prop.SetValue(unionMan, prop.GetValue(people[0], null), null); } } } } 

As you pointed out, you are already installing “invalid” vlaues in the default constructor, so you don’t have to worry about them inside this loop.

Note: In my example, I used versions of GetField and GetProperties that do not accept BindingFlags . They will only return public members.

+1
source

Source: https://habr.com/ru/post/921988/


All Articles