I'm new to C#, just a question on how to use IComparable and IComparer properly. Let's say we have the following class:
public class Student
{
int score;
string name;
}
and I want to sort by socre (desc) first then sort by name (asc).
Now assume that I can't access to the Student class, so I can only use IComparer
so I have to make one help class(let's say it is called StudentComparer) and put the same logic into
public class StudentComparer: IComparer
{
int Compare(object o1, object o2)
{
Student s1 = o1 as Student;
Student s2 = o2 as Student;
// not checking null for simplicity
if (s1.score == s2.score)
return String.Compare(s1.name, s2.name);
else if (s1.score < s2.score)
return -1
else
return 1
}
}
Here is my question, if I just need to use a single rule later, for example, sometimes just sort by name and sometimes just sort by score. So I have to make another two help classes(ScoreComparer and NameComparer) that implement IComparer, which have duplicated code in StudentComparer as
public class ScoreComparer : IComparer
{
int Compare(object o1, object o2)
{
//half logic of StudentComparer
}
}
public class NameComparer: IComparer
{
int Compare(object o1, object o2)
{
//half logic of StudentComparer
}
}
my case is pretty simple, image that if there is a complicated case, each comparer consist of hundreds line of code,so how can I avoid duplicated code? or is it a way to combine multiple Comparer A, B, C, D ... into a common Comparer so that it check A,B,C,D in sequence, just like order by clause in SQL
Back in the days before linq, I used to use comparers a lot.
The way I've handled different sort options was by specifying these options in the constructor of the comparer implementation and then use that information in the Compare method.
Lucky for us, we now have linq so this entire thing can be done with a single, fluent line of code:
// sortedStudents is an IEnumerable<Student> sorted by score and name.
var sortedStudents = students.OrderBy(s => s.Score).ThenBy(s => s.Name);
However, if for some reason you need to work the old-fashion way, using comparers and stuff like that, here is how I would handle this back then:
internal enum CompareBy
{
NameOnly,
ScoreAndName
}
public class StudentComparer: IComparer<Student>
{
private CompareBy _compareBy
public StudentComparer(CompareBy compareBy)
{
_compareBy = compareBy;
}
public int Compare(Student s1, Student s2)
{
// not checking null for simplicity
var nameCompare = string.Compare(s1.name, s2.name);
if(_compareBy == NameOnly)
{
return nameCompare;
}
// since there are only two members in the enum it's safe to write it like this.
// if the enum grows, you must change the code.
if (s1.score == s2.score)
{
return nameCompare;
}
else if (s1.score < s2.score)
{
return -1
}
return 1
}
}
Related
I am populating an array with instances of a class:
BankAccount[] a;
. . .
a = new BankAccount[]
{
new BankAccount("George Smith", 500m),
new BankAccount("Sid Zimmerman", 300m)
};
Once I populate this array, I would like to sort it by balance amounts. In order to do that, I would like to be able to check whether each element is sortable using IComparable.
I need to do this using interfaces. So far I have the following code:
public interface IComparable
{
decimal CompareTo(BankAccount obj);
}
But I'm not sure if this is the right solution. Any advice?
You should not define IComparable yourself. It is already defined. Rather, you need to implement IComparable on your BankAccount class.
Where you defined the class BankAccount, make sure it implements the IComparable interface. Then write BankAccount.CompareTo to compare the balance amounts of the two objects.
public class BankAccount : IComparable<BankAccount>
{
[...]
public int CompareTo(BankAccount that)
{
if (this.Balance < that.Balance) return -1;
if (this.Balance == that.Balance) return 0;
return 1;
}
}
Edit to show Jeffrey L Whitledge's solution from comments:
public class BankAccount : IComparable<BankAccount>
{
[...]
public int CompareTo(BankAccount that)
{
return this.Balance.CompareTo(that.Balance);
}
}
IComparable already exists in .NET with this definition of CompareTo
int CompareTo(Object obj)
You are not supposed to create the interface -- you are supposed to implement it.
public class BankAccount : IComparable {
int CompareTo(Object obj) {
// return Less than zero if this object
// is less than the object specified by the CompareTo method.
// return Zero if this object is equal to the object
// specified by the CompareTo method.
// return Greater than zero if this object is greater than
// the object specified by the CompareTo method.
}
}
Do you want to destructively sort the array? That is, do you want to actually change the order of the items in the array? Or do you just want a list of the items in a particular order, without destroying the original order?
I would suggest that it is almost always better to do the latter. Consider using LINQ for a non-destructive ordering. (And consider using a more meaningful variable name than "a".)
BankAccount[] bankAccounts = { whatever };
var sortedByBalance = from bankAccount in bankAccounts
orderby bankAccount.Balance
select bankAccount;
Display(sortedByBalance);
An alternative is to use LINQ and skip implementing IComparable altogether:
BankAccount[] sorted = a.OrderBy(ba => ba.Balance).ToArray();
There is already IComparable<T>, but you should ideally support both IComparable<T> and IComparable. Using the inbuilt Comparer<T>.Default is generally an easier option. Array.Sort, for example, will accept such a comparer.
If you only need to sort these BankAccounts, use LINQ like following
BankAccount[] a = new BankAccount[]
{
new BankAccount("George Smith", 500m),
new BankAccount("Sid Zimmerman", 300m)
};
a = a.OrderBy(bank => bank.Balance).ToArray();
If you need to compare multiple fields, you can get some help from the compiler by using the new tuple syntax:
public int CompareTo(BankAccount other) =>
(Name, Balance).CompareTo(
(other.Name, other.Balance));
This scales to any number of properties, and it will compare them one-by-one as you would expect, saving you from having to implement many if-statements.
Note that you can use this tuple syntax to implement other members as well, for example GetHashCode. Just construct the tuple and call GetHashCode on it.
This is an example to the multiple fields solution provided by #Daniel Lidström by using tuple:
public static void Main1()
{
BankAccount[] accounts = new BankAccount[]
{
new BankAccount()
{
Name = "Jack", Balance =150.08M
}, new BankAccount()
{
Name = "James",Balance =70.45M
}, new BankAccount()
{
Name = "Mary",Balance =200.01M
}, new BankAccount()
{
Name = "John",Balance =200.01M
}};
Array.Sort(accounts);
Array.ForEach(accounts, x => Console.WriteLine($"{x.Name} {x.Balance}"));
}
}
public class BankAccount : IComparable<BankAccount>
{
public string Name { get; set; }
public int Balance { get; set; }
public int CompareTo(BankAccount other) =>
(Balance,Name).CompareTo(
(other.Balance,other.Name ));
}
Try it
I am trying to get a list of distinct items from a custom collection, however the comparison seems to be getting ignored as I keep getting duplicates appearing in my list. I have debugged the code and I can clearly see that the values in the list that I am comparing are equal...
NOTE: The Id and Id2 values are strings
Custom Comparer:
public class UpsellSimpleComparer : IEqualityComparer<UpsellProduct>
{
public bool Equals(UpsellProduct x, UpsellProduct y)
{
return x.Id == y.Id && x.Id2 == y.Id2;
}
public int GetHashCode(UpsellProduct obj)
{
return obj.GetHashCode();
}
}
Calling code:
var upsellProducts = (Settings.SelectedSeatingPageGuids.Contains(CurrentItem.ID.ToString())) ?
GetAOSUpsellProducts(selectedProductIds) : GetGeneralUpsellProducts(selectedProductIds);
// we use a special comparer here so that same items are not included
var comparer = new UpsellSimpleComparer();
return upsellProducts.Distinct(comparer);
Most likely UpsellProduct has default implementation of GetHashCode that returns unique value for each instance of reference type.
To fix - either implement one correctly in UpsellProduct or in comparer.
public class UpsellSimpleComparer : IEqualityComparer<UpsellProduct>
{
public bool Equals(UpsellProduct x, UpsellProduct y)
{
return x.Id == y.Id && x.Id2 == y.Id2;
}
// sample, correct GetHashCode is a bit more complex
public int GetHashCode(UpsellProduct obj)
{
return obj.Id.GetHashCode() ^ obj.Id2.GetHashCode();
}
}
Note for better code to compute combined GetHashCode check Concise way to combine field hashcodes? and Is it possible to combine hash codes for private members to generate a new hash code?
Your GetHashCode() doesn't return the same values even two UpsellProduct instances are consider equals by your Equals() method.
Use something like this to reflect the same logic instead.
public int GetHashCode(UpsellProduct obj)
{
return obj.Id.GetHashCode() ^ obj.Id2.GetHashCode();
}
Say I have a list of Person objects:
class person
{
int id;
string FirstName;
string LastName;
}
How would I sort this list by the LastName member?
List<Person> myPeople = GetMyPeople();
myPeople.Sort(/* what goes here? */);
List<T>.Sort will sort the list in-place. If that's what you want, sweet: use the overload that takes a Comparison<T> delegate:
List<Person> myPeople = GetMyPeople();
myPeople.Sort((x, y) => x.LastName.CompareTo(y.LastName));
If you don't want to modify the actual list but want to enumerate over its contents in a sorted order, then, as others have suggested, use the LINQ extension method OrderBy (.NET 3.5 and up) instead. This method can take a simple Func<T, TKey> selector and order by the keys which this selector... you know, selects.
List<Person> myPeople = GetMyPeople();
var orderedByLastName = myPeople.OrderBy(p => p.LastName);
Under C# 3.0 you can use the following linq statement:
var sortedEnumerable = myPeople.OrderBy( p => p.LastName );
As others have suggested, you can use a custom IComparer method to sort your list. If you're only doing it once, the easiest way is using an anonymous method like such:
List<Person> myPeople = GetMyPeople();
myPeople.Sort(delegate(Person one, Person two)
{
return one.LastName.CompareTo(two.LastName);
});
Alternatively, you can implement IComparable on your Person class, so that the class knows how to sort itself:
class Person : IComparable<Person>
{
int id;
string FirstName;
string LastName;
public int CompareTo(Person other)
{
return LastName.CompareTo(other.LastName);
}
}
One option is to write a comparer:
class LastNameComparer : IComparer<Person>
{
public int Compare(Person x, Person y)
{
return String.Compare(x.LastName, y.LastName);
}
}
An then
myPeople.Sort(new LastNameComparer());
Person may also implement IComparable<Person>, in which case myPeople.Sort() will suffice. However, you may want to sort by other properties on other places, so this isn't a general method; if you want to sort by ID on another report you can write another IComparer, but you can only have one IComparable<Person>.CompareTo(Person other) method.
If you're feeling lazy, or sure you won't use it again, you can also use a lambda:
myPeople.Sort((p1, p2) => String.Compare(p1.LastName, p2.LastName));
A custom IComparer<person>. If you are using c#3.0 and .net3.5, have a look into the LINQ Methods, espescially IEnumerable<T>.OrderBy().
example of ICompare
/// <summary>
/// Sort class on LastName Descending Order
/// </summary>
private class PersonLastNameDescending : IComparer<Person>
{
public int Compare(Person aX, Person aY)
{
return aY.LastName.CompareTo(aX.LastName);
} // end Compare(...)
} // end inner class PersonLastNameDescending
MyPeople.Sort(new PersonLastNameDescending());
Nothing needs to go inside your "Sort" method call if your class implements IComparable.
Check this link for detail.
Code:
class person : IComparable<person>
{
int id;
string FirstName;
string LastName;
public int CompareTo(person other)
{
//If you want to sort on FirstName
return FirstName.CompareTo(other.FirstName);
}
}
I need an ordered queue where objects would be ordered by primary and secondary value.
class Object
{
int PrimaryValue;
int SecondaryValue;
}
The position of an Object in the queue must be determined by PrimaryValue. Object with higher PrimaryValue must preceed object with lower PrimaryValue. However for two objects with the same PrimaryValue a SecondaryValue must be used to determine precedence. Also I need two functions to get forward iterator GetFirst() and backward iterator GetLast() that would return respective iterators.
class Obj : IComparable<Obj>
{
int PrimaryValue;
int SecondaryValue;
public int CompareTo(Obj other)
{
if (other == null) throw new ArgumentNullException("other");
int diff = PrimaryValue - other.PrimaryValue;
return diff != 0 ? diff : SecondaryValue - other.SecondaryValue;
}
}
I'm not sure quite what you mean by forward and reverse iterators, which is C++ jargon for concepts that don't really exist in C#. You can always iterate over a collection in the forward direction simply by using foreach (var e in coll) ..., and in reverse by using System.Linq: foreach (var e in coll.Reverse()) ....
Sounds like what you want is either a PriorityQueue with the priority being a Pair or simply a SortedList with a custom Comparer. Here's an implementation of a PriorityQueue that could be adapted to your needs. Since GetEnumerator() returns an IEnumerable you can use the Reverse() extension method to iterate over it from back to front.
Similarly with the SortedList -- you need only supply a suitable IComparer that performs the comparison you need and use Reverse() for back to front iteration.
You can just use a List<T>, and call Sort(), however, to do so, instead implement IComparable<T> on your class. Finally, if you want to enumerate in reverse, just call Reverse() on the List<T>.
public class MyObject : IComparable<MyObject>
{
public int First;
public int Second;
public int CompareTo(MyObject other)
{
if (Equals(this, other))
{
return 0;
}
if (ReferenceEquals(other, null))
{
return 1;
}
int first = this.First.CompareTo(other.First);
if (first != 0)
{
return first;
}
return this.Second.CompareTo(other.Second);
}
}
you just need a SortedList....
and to give it your own copareing thingy...
http://msdn.microsoft.com/en-us/library/ms132323.aspx
I have two IEnumerable<T>s.
One gets filled with the fallback ellements. This one will always contain the most elements.
The other one will get filled depending on some parameters and will possibly contain less elements.
If an element doesn't exist in the second one, I need to fill it with the equivalent one of the first one.
This code does the job, but feels inefficient to me and requires me to cast the IEnumerables to ILists or to use a temporary list
Person implements IEquatable
IEnumerable<Person> fallBack = Repository.GetPersons();
IList<Person> translated = Repository.GetPersons(language).ToList();
foreach (Person person in fallBack)
{
if (!translated.Any(p=>p.equals(person)))
translated.add(person);
}
Any suggestions?
translated.Union(fallback)
or (if Person doesn't implement IEquatable<Person> by ID)
translated.Union(fallback, PersonComparer.Instance)
where PersonComparer is:
public class PersonComparer : IEqualityComparer<Person>
{
public static readonly PersonComparer Instance = new PersonComparer();
// We don't need any more instances
private PersonComparer() {}
public int GetHashCode(Person p)
{
return p.id;
}
public bool Equals(Person p1, Person p2)
{
if (Object.ReferenceEquals(p1, p2))
{
return true;
}
if (Object.ReferenceEquals(p1, null) ||
Object.ReferenceEquals(p2, null))
{
return false;
}
return p1.id == p2.id;
}
}
Try this.
public static IEnumerable<Person> SmartCombine(IEnumerable<Person> fallback, IEnumerable<Person> translated) {
return translated.Concat(fallback.Where(p => !translated.Any(x => x.id.equals(p.id)));
}
use Concat. Union does not work in case List<dynamic> type