Comparison<T, U> to Comparison<T>

J

jehugaleahsa

Hello:

I am writing a pretty large algorithms library for my personal use.

I am currently going through my algorithms and creating common
convenience overloads of those methods.

I have a LowerBound method that looks like this:

public static int LowerBound<TElement, TSearch>(IList<TElement> list,
TSearch search,
Comparison<TElement, TSearch> comparison) { ... }

This method allows me to search for a value within a list using
something other than the element type as my search criteria. This is
convenient when you have a class like Customer, but you want to search
them based on the customer ID. I can say something like this:

int lowerBound = LowerBound<Customer, int>(customers, 123, delegate
(Customer customer, int id)
{
return Comparer<int>.Default.Compare(customer.Id, id);
});

But, when I am just searching within a list of primitive types, the
extra generic argument is simply an annoyance.

I would like to create an overload that looks like this:

public static int LowerBound<TElement>(IList<TElement> list,
TElement search,
Comparison<TElement> comparison) {...}

I want to be able to call the other overload so I don't have to
duplicate my code. The problem is that the compiler does not like
converting Comparison<T> to a Comparison<T, T>, even though this is
perfectly logical in my situation.

Is there a way to force the compile to accept this without wrapping
the delegate inside another. In other words, I don't want to do this:

public static int LowerBound<TElement>(IList<TElement> list,
TElement search,
Comparison<TElement> comparison)
{
return LowerBound<TElement, TElement>(list,
search,
delegate(TElement value1, TElement value2)
{
return comparison(value1, value2);
});
}

Any suggestions?

Thanks,
Travis
 
J

jehugaleahsa

I am writing a pretty large algorithms library for my personal use.
I am currently going through my algorithms and creating common
convenience overloads of those methods.
I have a LowerBound method that looks like this:
public static int LowerBound<TElement, TSearch>(IList<TElement> list,
    TSearch search,
    Comparison<TElement, TSearch> comparison) { ... }
This method allows me to search for a value within a list using
something other than the element type as my search criteria.

I'm afraid I don't really understand the usage of this method.  What is 
the signature for your "Comparison<TElement, TSearch>" delegate type?  
What is the code inside your LowerBound<TElement, TSearch> method?

I suspect that there's no need for you to implement this method at all,  
but without knowing exactly what it does, it's hard to know for sure.  
.NET has lots of collection-sifting methods to do a wide variety of  
operations, and usually one of those suffices.

As far as the specific question goes...
[...]
I want to be able to call the other overload so I don't have to
duplicate my code. The problem is that the compiler does not like
converting Comparison<T> to a Comparison<T, T>, even though this is
perfectly logical in my situation.

All due respect, your last sentence is a bit of a non-sequitur.  There's a  
big difference between what you may want to do semantically, and what a  
programming language can be designed to accept.  In particular, it's not  
logical at all that one delegate type should be automatically convertible 
to some other delegate type.  How could the compiler possibly know what 
your intent is when trying to call a single-argument method with a  
two-argument delegate?  Only by explicitly showing the compiler exactly 
what you intend can it be done, and of course that means wrapping the  
delegate just as you've done.

Now, all that said, I'm sure there are refinements that be made here to  
simplify things, and quite possible to get rid of the method altogether.  
But to do that would require knowing exactly what the method does and what  
the delegate type you've declared is supposed to do (a reasonable guess  
might be that your Comparison<TElement, TSearch> has the same semantics as  
the built-in Comparison<T> type, but it's hard to see how that would fit  
into your "LowerBound" method, so really we just need a complete  
description of what's going on here).

Pete- Hide quoted text -

- Show quoted text -

Lower bound is the C++ term for returning the smallest index into a
sorted list such that inserting the search value at that index would
result in the list remaining sorted. It is similar to binary search,
except that it doesn't care whether the value exists or not in the
list; it always returns a non-negative number. Here is the private
method my public method calls.

private static int lowerBound<TElement, TSearch>
(IList<TElement> list,
TSearch value,
Comparison<TElement, TSearch> comparison)
{
int first = 0;
int count = list.Count;
while (0 < count)
{
int half = count / 2;
int middle = first + half;
if (comparison(list[middle], value) < 0)
{
++middle;
first = middle;
count -= half + 1;
}
else
{
count = half;
}
}
return first;
}

The big problem with using .NET's binary search is that it may or may
not return the first possible index. It is not guaranteed to handle
duplicates, in other words. Even if it did, it would only work with a
List<T> or an Array, specifically - not a generic IList<T>.

In my situation I have a delegate that looks like this:

public delegate int Comparison<TValue, TSearch>(TValue value, TSearch
search);

..NET has a delegate that looks like this:

public delegate int Comparison<T>(T x, T y);

When TValue is the same type as TSearch, the method signatures are the
same. It would be nice if there was a way to tell the compiler, "Hey,
the method signatures required by these different delegates are the
same. It's okay!".

I don't really want a debate about the genuine need for such a thing;
I am just saying it would be convenient in my situation.

This is for my personal use, so I don't see why it is anybody else's
business what I do with my code. I've already deemed that I want to
write my code this way so there is no point looking for other
solutions. I'm just trying to make the signature of the method less of
an eye-sore.

I just asked a question; that's all.
 
P

Pavel Minaev

In my situation I have a delegate that looks like this:

public delegate int Comparison<TValue, TSearch>(TValue value, TSearch
search);

.NET has a delegate that looks like this:

public delegate int Comparison<T>(T x, T y);

When TValue is the same type as TSearch, the method signatures are the
same. It would be nice if there was a way to tell the compiler, "Hey,
the method signatures required by these different delegates are the
same. It's okay!".

You can do that, though (as you've already found out), not with a
plain cast - instead, you need to remember the days of C# 1.0, and use
"new" together with the delegate type:

Comparison<int, int> x;
Comparison<int> y = new Comparison<int>(x);

There's no way to make this conversion implicit, unfortunately - C#
type system is strictly nominal, not structural, even for delegate
types. It is an explicit design decision by C# team (I recall Eric
Lippert blogging about that once), though personally I believe it
doesn't really make that much sense.
 
J

jehugaleahsa

Lower bound is the C++ term for returning the smallest index into a
sorted list such that inserting the search value at that index would
result in the list remaining sorted.

No, actually it's not.  "Lower bound" is a broadly general phrase, usedin  
the context of all sorts of programming languages, to mean a wide variety 
of different things.  In fact, in C/C++, the standard name for the binary  
search function is "bsearch".  That's the name of the function providedin  
the CRT library.
It is similar to binary search,
except that it doesn't care whether the value exists or not in the
list;

There's nothing about "binary search" that implies the searched-for  
element is in the collection being searched.  In other words, what you  
have _is_ a binary search.
[...]
The big problem with using .NET's binary search is that it may or may
not return the first possible index. It is not guaranteed to handle
duplicates, in other words. Even if it did, it would only work with a
List<T> or an Array, specifically - not a generic IList<T>.

That's all true.  It's unfortunate that the BinarySearch() methods are  
implementations on specific classes.  I would guess that if they had done  
it now, with extension methods providing a way to effectively implement  
methods for interfaces instead of only on classes, it would be more  
convenient.

That said, since it's trivial to scan backwards to find the first element 
once any element has been found, I'm not entirely convinced it's a good  
use of your time to reimplement the entire binary search algorithm, just  
to get that behavior.  :)  But if you really want to, or have to support  
In my situation I have a delegate that looks like this:
public delegate int Comparison<TValue, TSearch>(TValue value, TSearch
search);
.NET has a delegate that looks like this:
public delegate int Comparison<T>(T x, T y);
When TValue is the same type as TSearch, the method signatures are the
same. It would be nice if there was a way to tell the compiler, "Hey,
the method signatures required by these different delegates are the
same. It's okay!".

Well, unfortunately that's just not how the type compatibility works in C#.

C# current has limited support for contravariance for generic delegate  
types (for example, you can assign an EventHandler<EventArgs>-derived  
value to an EventHandler<EventArgs> variable), and it appears that v4.0  
will introduce some support for covariant generics.

But that compatibility depends on an inheritance relationship between the 
types.  What you're asking for is more like this:

     class A<T>
     {
         public T i;
         public T j;
     }

     class A<T, U>
     {
         public T i;
         public U j;
     }

and being able to write code like this:

     A<T, U> a1 = new A<T, U>();
     A<T> a2 = a1;

Sure, by human inspection we can tell that these types are essentially the  
same.  But they aren't actually _the same type_, nor are they inheriting  
 from each other (i.e. one is not a subclass of another).  We have the same  
problem with the delegates.  A delegate type is a special kind of type, 
but it's still a type, and the same type-compatibility rules have to apply.

C#/.NET also has type conversion (a special kind of compatibility,  
different from casting), and you can provide user-defined conversions with  
the "implicit" and "explicit" operators.  But you can only define a  
conversion either to or from the type in which the conversion is  
declared.  .NET doesn't already have a conversion like what you want, and  
you can't implement it because you can't inherit delegate types.

You could do something similar with extension methods, providing a special  
"Convert" method that does the wrapping.  This doesn't avoid the wrapping,  
but it would at least isolate that code, so that you could reuse it  
wherever the situation comes up.

For that matter, the extension method could get really fancy and actually 
create a new delegate instance using the target method of the passed-in  
delegate.  In other words, rather than returning a new delegate instance  
that simply retains and calls the original delegate instance, you could  
create a delegate instance that extracts the necessary information from  
the original, incorporating that into a new delegate without having to  
retain a reference to the original delegate instance.

Now, all that said, I am still not convinced that in this case you are  
really in need of this behavior.  In particular, there's not really any 
reason that your binary search method needs access to the specific element  
being searched for at all.  It only ever uses it by passing it to the  
comparison method, and so all you really need is a a delegate like this:

     // Name this whatever you want, but since it takes just the one
     // argument, I'd try to avoid something as generic as simply  
"Comparison"
     delegate int BinarySearchComparison<T>(T t);

In your "LowerBound()" method, you'd use the above as the delegate  
argument, rather than Comparison<T, U> or Comparison<T>, and you'd simply 
pass the current element, rather than the current element and some value  
passed to the method (and of course the method would only have the two  
arguments, with the TSearch element excluded altogether).

Then, it would be used something like this (equivalent to using  
Comparison<T, U>):

     int i = 123;
     int lowerBound = LowerBound(customers, customer =>  
customer.Id.CompareTo(i));

or this (equivalent to using Comparison<T>):

     Customer customerToFind = ...;
     int lowerBound = LowerBound(customers, customer =>  
customer.CompareTo(customerToFind));

A couple of points: all that Comparer<T>.Default does is return a Comparer  
instance that uses IComparable<T> on the type given.  So if you aren't in  
a situation where you need to pass something an actual Comparer instance, 
you might as well just call IComparable<T> directly (as above).

Also note that I used the lambda expression syntax, which for stuff like  
this helps make things somewhat more concise than the traditional  
"delegate { }" anonymous method declaration.

Also note that the compiler can infer the type parameter for the method  
 from the arguments given, so there's no need to provide them in this  
situation.

But most relevantly, note that I've incorporated the value you're looking 
for directly in the lambda expression, rather than depending on the  
LowerBound() method to pass that value in.  The value is invariant for the  
operation, and the search method itself never actually cares about it.  So  
there's no need to bother including it at all.

Other than the usual caveats about captured variables (which is what  
happens here to your variable "i"), this is a much cleaner approach IMHO, 
and side-steps the entire question of having a Comparison<T, U> versus a  
I don't really want a debate about the genuine need for such a thing;
I am just saying it would be convenient in my situation.

And I'm just saying that there are probably ways to address the situation 
in ways that are _even more_ convenient than what you have in mind.
This is for my personal use, so I don't see why it is anybody else's
business what I do with my code.

Frankly, that statement comes across as a bit hostile.  I'm not sure  
that's the impression you really want to make when all I or anyone else  
here is trying to do is help.

But beyond that, it's "anybody else's business" because you made it our  
business.  You asked a question in the context of how to implement your 
code, and that question can only be correctly answered with a reasonably  
complete understanding of how you're using your code and what the actual  
implementation is.
I've already deemed that I want to
write my code this way so there is no point looking for other
solutions.

Even if other solutions are superior to the approach you're using, meeting  
the exact goals you're trying to achieve in even more convenient ways?
I'm just trying to make the signature of the method less of
an eye-sore.

But that's exactly what I'm trying to help you do.
I just asked a question; that's all.

And I've just answered the question, that's all.  I don't see any reason  
for you to get so defensive about it.

The fact is, as the person who is asking the question, you are in a VERY  
poor position to make a determination as to what information is or is not 
needed in order to properly answer the question.  Raising a big stink just  
because someone wants additional information about your question is  
pointless at best and needlessly antagonistic at worst.

Pete

I like your ideas. Sorry I got defensive. Some times I feel like you
personally trying to disqualify my questions. Some times you have
perfectly good reason to! You concentrated on the purpose of the
method rather than my question and that was frustrating. A simple,
"No, you can't cast between delegates" would have sufficed. However,
you did provide some very good ideas about ways to avoid delegates
altogether. For that I am humbled and hope you don't take my
defensiveness personally. You and I have been neck-to-neck in the
past, so it has given me a bad impression of your advice (although it
is usually quite thorough).

Next time I will try to be a little more patient. Oh! And I call it
lower bound because that is what it is called in the Standard Template
Library. I can't just back up in the list after a Binary Search
because it could result in poor performance, say, if there were 1000
duplicates (very unlikely).

Thanks again!
 
J

jehugaleahsa

You can do that, though (as you've already found out), not with a
plain cast - instead, you need to remember the days of C# 1.0, and use
"new" together with the delegate type:

   Comparison<int, int> x;
   Comparison<int> y = new Comparison<int>(x);

There's no way to make this conversion implicit, unfortunately - C#
type system is strictly nominal, not structural, even for delegate
types. It is an explicit design decision by C# team (I recall Eric
Lippert blogging about that once), though personally I believe it
doesn't really make that much sense.

The problem with this is that it wraps. Double indirection could make
my applications slower (however unlikely). We have a policy at work to
always ask the question whether or not a design decision is for the
developer's convenience or to improve the system. I see this as more
of a developer convenience, since it only helps during development and
costs runtime. It is a little annoying when you know 99.9% of the time
these types of things don't make a difference.

I kind of like Peter's suggestion about hard-coding the search value
in the delegate itself. Of course, I would rather the search value be
passed as an argument... I'm not sure which way I'm willing to
sacrifice yet.

Thanks for your input.
 
P

Pavel Minaev

Okay...I still haven't read the CLR spec to find out exactly what  
happens.  But, I did a quick-and-dirty test that suggests I'm wrong, and  
that the new delegate instance does in fact continue to reference the  
original one.

This is interesting. Can you post the code of the test?
 
P

Pavel Minaev

For bonus points, I included an extension method that creates a new  
instance of the delegate _without_ wrapping the old one (I made no attempt  
to support multicast delegates though...I leave that as an exercise for  
the reader :) ).

Caveat: this test does not _prove_ that there's a double-indirection when 
invoking the delegate.  All I've done is check to see whether the original  
delegate instance remains reachable (and thus is not collected by the GC  
when I force a collection).

All well and good, except for a slight problem - GC.Collect() call is
asynchronous, and the cleanup might have not finished by the time you
check :)

Indeed, on my system here, the test consistently prints:

original reference collected: True
 This strongly suggests that the new delegate  
is simply wrapping the original and invoking it each time the new delegate  
is invoked (after all, why else would the new delegate continue to  
reference the original?).  But to know this for sure, you'd have to  
actually look at the implementation (and I personally just don't have the 
time or motivation at the moment to do that :) ).

Actually, I think a simpler check would do - just dump the invocation
list for action2. If it references action1, the method name will be
Invoke (of that delegate). If it references the original method, it
will be Method.

I've actually tried it, inserting this:

Console.WriteLine(action2.Method);

after your check. And sure enough, here's the output:

Void Method(Int32)

So it would seem that no double indirection occurs. This is .NET 3.5
SP1, naturally, but I'd expect it to behave the same on all versions.
Pete

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

namespace TestDelegateWrapCopy
{
     class Program
     {
         delegate void MyAction<T>(T t);

         static void Main(string[] args)
         {
             Action<int> action1 = Method;
             WeakReference weak = new WeakReference(action1);
             MyAction<int> action2 = new MyAction<int>(Method);
             MyAction<int> action3 = action1.ConvertTo<MyAction<int>>();

             action1 = null;
             GC.Collect(2, GCCollectionMode.Forced);

             action2(0);
             action3(1);
             Console.WriteLine("original reference collected: " +  
!weak.IsAlive);
             Console.ReadLine();

             GC.KeepAlive(action1);
             GC.KeepAlive(action2);
             GC.KeepAlive(action3);
         }

         static void Method(int i)
         {
             Console.WriteLine("called Method({0})", i);
         }
     }

     static class Extensions
     {
         public static T ConvertTo<T>(this Delegate del)
         {
             if (!typeof(T).IsSubclassOf(typeof(Delegate)))
             {
                 throw new ArgumentException("Can only convert to another  
Delegate type");
             }

             if (del.Target != null)
             {
                 return (T)(object)Delegate.CreateDelegate(typeof(T),  
del.Target, del.Method);
             }
             else
             {
                 return (T)(object)Delegate.CreateDelegate(typeof(T),  
del.Method);
             }
         }
     }



}
 
P

Pavel Minaev

I'm not sure that's true.  My recollection is that while garbage  
collection is normally done asynchronously (sort of...the GC may wind up  
suspending or even hijacking a thread so that collection can happen  
safely), the GC.Collect() method specifically waits until collection has  
completed.

I went on to check this. The results are very much unexpected, and do
not shed much light on the problem. Here's the code that I've used:

using System;
using System.Linq;

class Program2
{
static void Main()
{
var srs = new object[1000000];
var wrs = new WeakReference[srs.Length];

for (int i = 0; i < srs.Length; ++i)
wrs = new WeakReference(srs = new object());

GC.KeepAlive(srs);
srs = null;
GC.Collect();

Console.WriteLine("ESC to exit");
do
{
int n = 0;
int? first = null;
for (int i = 0; i < wrs.Length; ++i)
{
if (wrs.IsAlive)
{
first = first ?? i;
++n;
}
}
Console.WriteLine("{0} objects still alive, first={1}", n,
first);
} while (Console.ReadKey().KeyChar != (char)27);

GC.KeepAlive(srs);
GC.KeepAlive(wrs);
}
}

I reasoned that, either I get "0 objects alive" printed - which means
that GC.Collect() fully cleans up; or I get "K objects alive" for some
K>0, which means it does not.

Well, it turns out that the damn thing always prints "1 objects
alive", no matter how long you wait after the Collect call, and no
matter how many objects were created! Furthermore, I've printed the
index of that object, and it's always the last object in the array -
again, regardless of the size of the array.

Does anyone has any idea of what is going on here?
Did you change the initialization of the "action2" variable to reference  
"action1" instead of "Method"?

Oh.. gosh. *blush* I skimmed past that part, not looking closely. Yes,
now I observe the same behavior. In fact, my additional check for the
invocation list also shows that the new delegate actually does
reference Invoke. Here's the complete output:

original reference collected: False
Void Invoke(Int32)
 
J

jehugaleahsa

I'm not sure that's true.  My recollection is that while garbage  
collection is normally done asynchronously (sort of...the GC may wind up  
suspending or even hijacking a thread so that collection can happen  
safely), the GC.Collect() method specifically waits until collection has  
completed.

The _finalizers_ for collected objects may not yet have run, thus the GC  
method WaitForPendingFinalizers().  But I am under the impression that  
GC.Collect() is essentially synchronous.  Unfortunately, the documentation  
isn't clear about it, but I think that's correct.  Certainly all of the 
pages that I've seen that discuss GC.Collect() imply that the method  
doesn't return until the collection has been completed.

But if it makes you nervous, you can always add code to request  
notifications of approaching GC operations and wait for a full GC.  Of  
course, that requires disabling concurrent GC, which involves messing with  
the config file for the application, and you'll need to add more code to  
synchronize the thread waiting for notification and the main thread doing 
the work here.  More hassle than I care to bother with, given what elseI  
know about the problem...



Did you change the initialization of the "action2" variable to reference  
"action1" instead of "Method"?

Sorry for not being more clear...I was in a hurry and just  
copied-and-pasted the code without elaborating on it.  In its posted  
state, you _should_ expect the reference to be GC'ed, because it wasn't  
used to initialize the second delegate instance.  My intent was to execute  
the code twice, so you can compare the behavior.  It just happened thatI  
left it in the "non-wrapping" state instead of the "wrapping" state.

So, if you modify the initializer from:

     MyAction<int> action2 = new MyAction<int>(Method);

to:

     MyAction<int> action2 = new MyAction<int>(action1);

You should (may) see that the original delegate reference does not get  
collected.

I readily admit that my test is not definitive.  Not only does it only  
tell you about the visible behavior, not the underlying implementation,  
because of the non-deterministic behavior of the GC even that visible  
behavior isn't necessarily specific enough to provide a definitive answer..

But it is strongly suggestive, IMHO.



Why do you say that?  It's entirely implementation-dependent.  Just as the  
invocation of the second delegate instance could be delegating to the  
original delegate instance, that second instance could also be delegating 
the Method property to the original delegate instance (and any other  
accessor).

Now, all that said, it turns out that if you initialize the second  
delegate instance with the original delegate instance, instead of "Method"  
as the code I posted does, you _do_ get a call to "Invoke" instead of the 
original method, just as you said should happen if the original delegate  
instance remains referenced.

In other words, the implementation is in fact exposed (even though it  
didn't have to be), and the run-time is telling us quite clearly that it  
_does_ wrap the delegate and _does_ produce a double-indirection when  
another delegate instance is used to initialize a new delegate instance.

Not that I think any of this should really matter to the OP.  The cost of  
the double-indirection is going to be incredibly tiny as compared to the  
cost of whatever the delegate is actually doing (for example, comparing  
two integers).  But he's consistently shown that it will matter to him  
regardless.  And given that, he will prefer at the very least the approach  
shown in the extension method I posted, if not avoiding the issue  
altogether by using the variable-capturing approach I suggested earlier.

Pete

I do tend to over obsess on petty things like performance, to my own
fault. Unfortunately I live in a development environment that requires
the regular use of a profiler. We have a calculation for our energy
transmission system that calculates the average peak load on the
entire system for each hour in a month. With close to 500,000 records
to process (with many more generated), time is precious in the low-
level functions. We have a collection class that holds a line in a
report (upwards of about 2000 lines per customer); it is constantly
being searched for a particular line. Fortunately, the lines are
always kept sorted, so a simple binary search finds us what we need.
Finally, there are modification codes on certain lines to represent
special items, which we also need to have quick access to. It is a
rather ugly data structure.

To shave off a little more time we were able to create a delayed sort,
where the list would only sort itself (with quick sort) when its
elements were accessed. It would only recognize that it needed to be
resorted when something was inserted out of order. This was great
because we could always place items at the end of the list, which
saved us from shifting. It is a faily complex data structure, but
anyone could write it. At the heart is a dumbed-down binary search and
quick sort.

When I first started working here, the data structure was a simple C
array (we've upgraded to C#). The array was searched linearly since,
in the past, the upper limit of records was closer to 200 (instead of
2000). Modification codes were looked up linearly as well, hard-coded
or were constantly re-queried off the database. The system ran for 5
hours. After the rewrite it runs in 10 minutes with far less database
activity.

Just FYI, I created another overload of the method that looks like
this:

public delegate int LowerBoundComparison<T>(T current);

public static int LowerBound<TElement>(IList<TElement> list,
LowerBoundComparison<TElement> comparison);

I agree that placing the search value within the comparison can be
(and probably always is) more efficient. I just needed to add a few
extra comments to explain why there isn't a parameter for the search
value. Considering most of my coworkers have trouble understanding the
role of delegates in the first place, I don't see this as that big of
a leap for them.

I have always had a lot of trouble explaining generic algorithms that
require delegates. It is hard to explain that delegates just "fill in
the blanks". Each method requires a slightly different filler. That is
why this project is mostly for my personal use. They can ask me when
they see something they don't understand in my code. 90% of the time,
I will probably not use this code directly at all. I will probably
just keep as some sort of reference. (It is nice to have an algorithm
out there when you really need it - tested, flexible and ready to
use.)

Today, the aforementioned data structure works with
List<T>.BinarySearch - I have been slowly training myself _NOT_ to
replace the built-in code. Unfortunately, this method forces you to
pass in NULL as the search value and a custom Comparison that knows
how to search for an item just looking at one of its properties.
....
int index = list.BinarySearch(null, new LineIdComparer("Line123"));
....
internal class LineIdComparer : IComparer<ILine>
{
private readonly string _lineId;

public CustomerComparer(string lineId)
{
_lineId = lineId;
}

// You don't know which argument it will be.
public int Compare(ILine x, ILine y)
{
if (x == null)
return Comparer<string>.Default.Compare(_lineId,
y.LineId);
else
return Comparer<string>.Default.Compare(x.LineId,
_lineId);
}
}
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top