Wo finde ich die "Clamp" -Funktion in .NET?

92

Ich möchte einen Wert xan einen Bereich klemmen [a, b]:

x = (x < a) ? a : ((x > b) ? b : x);

Das ist ziemlich einfach. Aber ich sehe keine Funktion "clamp" in der Klassenbibliothek - zumindest nicht in System.Math.

(Wenn Sie nicht wissen, dass ein Wert "geklemmt" werden soll, müssen Sie sicherstellen, dass er zwischen einigen Maximal- und Minimalwerten liegt. Wenn er größer als der Maximalwert ist, wird er durch den Maximalwert usw. ersetzt.)

Danvil
quelle
2
@ Danvil: Es gibt keine "C # -Klassenbibliothek". Sie meinen ".NET Framework".
John Saunders
1
Immer noch nichts ab C # 7.1?
Joce
1
@ JohnSaunders Ich glaube nicht, dass das absolut wahr ist stackoverflow.com/questions/807880/…
Adam Naylor
Wenn ich fragen würde, wie man einen Wert "begrenzt", würde jeder einzelne englischsprachige Programmierer auf der Welt sofort wissen, was ich meinte. Höchstwahrscheinlich würde jeder Programmierer es wissen. Nach mehr als 30 Jahren im Geschäft musste ich herausfinden, was "Klemme" heute bedeutet. Ähnlich wie bei "Abhängigkeitsinjektion" ist "Parametrisierung" so offensichtlich, dass niemand jemals ein Buch darüber geschrieben hat.
Bob
@ Bob Einige Wörter haben eine historische, gut definierte Bedeutung. Clamp ist einer von ihnen. en.wikipedia.org/wiki/Clamping_(graphics) oder khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml oder docs.microsoft.com/en-us/windows/win32/direct3dhlsl/… "Limit "wäre irreführend, insbesondere, dass" limit "in der Mathematik bereits eine andere Bedeutung hat.
Kaalus

Antworten:

135

Sie könnten eine Erweiterungsmethode schreiben:

public static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
{
    if (val.CompareTo(min) < 0) return min;
    else if(val.CompareTo(max) > 0) return max;
    else return val;
}

BEARBEITEN: Erweiterungsmethoden werden in statische Klassen eingeteilt. Da dies eine recht einfache Funktion ist, sollte sie wahrscheinlich in einem Kernnamespace Ihres Projekts enthalten sein. Sie können die Methode dann in jeder Codedatei verwenden, die eine using-Direktive für den Namespace enthält, z

using Core.ExtensionMethods

int i = 4.Clamp(1, 3);

.NET Core 2.0

Ab .NET Core 2.0 gibt es System.Mathjetzt eine ClampMethode, die stattdessen verwendet werden kann:

using System;

int i = Math.Clamp(4, 1, 3);
Lee
quelle
1
Wo würde ich das hinstellen und ruft CompareTo langsamer auf als den Vergleich mit <(für integrale Typen)?
Danvil
1
In einer statischen Klasse und im .NET-Framework (bei Mono, Compact usw. nicht sicher) sollte das Generikum für den Typ neu kompiliert und CompareTo inline geschrieben werden, damit keine Leistungseinbußen auftreten.
Robert Fraser
1
@Frasier Sofern es sich nicht um äußerst leistungsempfindlichen Code handelt, ist es unwahrscheinlich, dass Sie dadurch bedeutende Leistungssteigerungen erzielen. Generisch zu sein ist wahrscheinlich nützlicher als ein paar Mikrosekunden zu sparen.
MgSam
4
Das Gute an der Beschränkung auf die generische Version von IComparableist, dass kein Boxen auftritt. Dies sollte sehr schnell gehen. Denken Sie daran, dass die Methode mit doubleund einer Gesamtreihenfolge entspricht, in der weniger als alle anderen Werte enthalten sind, einschließlich . Es ist also nicht gleichbedeutend mit dem Bediener. Wenn Sie einen Gleitkommatyp verwenden, müssen Sie überlegen, wie Sie ihn ebenfalls behandeln möchten . Dies ist für andere numerische Typen nicht relevant. floatCompareToNaNNegativeInfinity<<NaN
Jeppe Stig Nielsen
1
NaNIn beiden Fällen müssen Sie überlegen, wie Sie behandeln sollen . Die Version mit <und >würde die Ausgabe NaNund Verwendung NaNfür minoder maxwürde effektiv eine einseitige Klemme machen. Mit CompareTowürde es immer wieder kommen NaNwenn es maxist NaN.
Herman
29

Verwenden Sie einfach Math.Minund Math.Max:

x = Math.Min(Math.Max(x, a), b);
d7samurai
quelle
Das bedeutet, int a0 = x > a ? x : a; return a0 < b ? a0 : bdass (obwohl es korrekte Ergebnisse liefert) nicht gerade ideal ist.
Mr. Smith
12
und warum ist das?
d7samurai
4
@ d7samurai Wenn wir wissen, dass min <= max ist, Math.Min(Math.Max(x, min), max)ergibt sich ein Vergleich mehr als nötig, wenn x <min.
Jim Balter
@ JimBalter, theoretisch ist das wahr. Wenn Sie sich ansehen, wie CompareTo () normalerweise implementiert wird, kann die akzeptierte Antwort bis zu 6 Vergleiche umfassen. Ich weiß allerdings nicht, ob der Compiler klug genug ist und CompareTo () einbindet und die überflüssigen Vergleiche entfernt.
Quinmars
1
Dies ist gut für Fälle, in denen Sie es nur einmal tun müssen, dann fühlt sich eine ganz neue Funktion dafür wie ein Overkill an.
Feos
26

Versuchen:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}
Klitoris
quelle
6
Pfui! Diese hässlichen überflüssigen Klammern! Wenn Sie mit den doppelten ternären Operatoren ein böses Genie sein wollen, machen Sie es zumindest richtig und entfernen Sie diese auch! 😂
XenoRo
8
@ XenoRo Diese "redundanten" Klammern machen es lesbar.
Klarer
2
@Cleaner - 1) Wenn Sie die Lesbarkeit verbessern möchten, werden doppelte Ternäre vermieden und stattdessen IF-Blöcke verwendet. 2) Du verstehst den Witz nicht, oder? xD
XenoRo
13

Es gibt keinen, aber es ist nicht zu schwer, einen zu machen. Ich habe hier eine gefunden: Klammer

Es ist:

public static T Clamp<T>(T value, T max, T min)
    where T : System.IComparable<T> {
        T result = value;
        if (value.CompareTo(max) > 0)
            result = max;
        if (value.CompareTo(min) < 0)
            result = min;
        return result;
    }

Und es kann verwendet werden wie:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5
Jeremy B.
quelle
Diese Lösung ist besser als die akzeptierte. Keine Mehrdeutigkeit.
Aggsol
6
@CodeClown Diese Lösung führt zu einem unnötigen Vergleich, wenn der Wert> max ist und die umgekehrte Argumentreihenfolge Fehler einlädt (und praktisch garantiert). Ich weiß nicht, welche Mehrdeutigkeit Ihrer Meinung nach vermieden wird.
Jim Balter
Um die Konsistenz mit der alten Math.Clamp-Implementierung zu gewährleisten, empfehlen wir, die Reihenfolge der Min / Max-Parameter zu Clamp(T value, T min, T max)
ändern
4

Teilen Sie einfach Lees Lösung mit den Problemen und Bedenken der Kommentare, wenn möglich:

public static T Clamped<T>(this T value, T min, T max) where T : IComparable<T> {
    if (value == null) throw new ArgumentNullException(nameof(value), "is null.");
    if (min == null) throw new ArgumentNullException(nameof(min), "is null.");
    if (max == null) throw new ArgumentNullException(nameof(max), "is null.");
    //If min <= max, clamp
    if (min.CompareTo(max) <= 0) return value.CompareTo(min) < 0 ? min : value.CompareTo(max) > 0 ? max : value;
    //If min > max, clamp on swapped min and max
    return value.CompareTo(max) < 0 ? max : value.CompareTo(min) > 0 ? min : value;
}

Unterschiede:

Einschränkungen: Keine einseitigen Klemmen. Wenn maxja NaN, wird immer zurückgegeben NaN(siehe Hermans Kommentar ).

XenoRo
quelle
Eine weitere Einschränkung ist, nameofdass sie für C # 5 oder niedriger nicht funktioniert.
RoLYroLLs
0

Unter Verwendung der vorherigen Antworten habe ich es für meine Bedürfnisse auf den folgenden Code komprimiert. Auf diese Weise können Sie eine Zahl auch nur um ihre minimalen oder maximalen Werte klemmen.

public static class IComparableExtensions
{
    public static T Clamped<T>(this T value, T min, T max) 
        where T : IComparable<T>
    {
        return value.CompareTo(min) < 0 ? min : value.ClampedMaximum(max);
    }

    public static T ClampedMinimum<T>(this T value, T min)
        where T : IComparable<T>
    {
        return value.CompareTo(min) < 0 ? min : value;
    }

    public static T ClampedMaximum<T>(this T value, T max)
        where T : IComparable<T>
    {
        return value.CompareTo(max) > 0 ? max : value;
    }
}
Bobby Speirs
quelle
Warum nicht return value.ClampedMinimum(min).ClampedMaximum(max);?
Henrik
0

Der folgende Code unterstützt die Angabe von Grenzen in beliebiger Reihenfolge (dh bound1 <= bound2oder bound2 <= bound1). Ich fand dies nützlich für Klemmwerte, die aus linearen Gleichungen ( y=mx+b) berechnet wurden, bei denen die Steigung der Linie zunehmen oder abnehmen kann.

Ich weiß: Der Code besteht aus fünf superhässlichen Operatoren für bedingte Ausdrücke . Die Sache ist, es funktioniert und die folgenden Tests beweisen es. Fühlen Sie sich frei, streng unnötige Klammern hinzuzufügen, wenn Sie dies wünschen.

Sie können problemlos andere Überladungen für andere numerische Typen erstellen und die Tests grundsätzlich kopieren / einfügen.

Warnung: Der Vergleich von Gleitkommazahlen ist nicht einfach. Dieser Code implementiert doubleVergleiche nicht robust. Verwenden Sie eine Gleitkomma-Vergleichsbibliothek, um die Verwendung von Vergleichsoperatoren zu ersetzen.

public static class MathExtensions
{
    public static double Clamp(this double value, double bound1, double bound2)
    {
        return bound1 <= bound2 ? value <= bound1 ? bound1 : value >= bound2 ? bound2 : value : value <= bound2 ? bound2 : value >= bound1 ? bound1 : value;
    }
}

xUnit / FluentAssertions-Tests:

public class MathExtensionsTests
{
    [Theory]
    [InlineData(0, 0, 0, 0)]
    [InlineData(0, 0, 2, 0)]
    [InlineData(-1, 0, 2, 0)]
    [InlineData(1, 0, 2, 1)]
    [InlineData(2, 0, 2, 2)]
    [InlineData(3, 0, 2, 2)]
    [InlineData(0, 2, 0, 0)]
    [InlineData(-1, 2, 0, 0)]
    [InlineData(1, 2, 0, 1)]
    [InlineData(2, 2, 0, 2)]
    [InlineData(3, 2, 0, 2)]
    public void MustClamp(double value, double bound1, double bound2, double expectedValue)
    {
        value.Clamp(bound1, bound2).Should().Be(expectedValue);
    }
}
NathanAldenSr
quelle
0

Wenn ich den Bereich eines Arguments in [min, max] validieren möchte, verwende ich die folgende praktische Klasse:

public class RangeLimit<T> where T : IComparable<T>
{
    public T Min { get; }
    public T Max { get; }
    public RangeLimit(T min, T max)
    {
        if (min.CompareTo(max) > 0)
            throw new InvalidOperationException("invalid range");
        Min = min;
        Max = max;
    }

    public void Validate(T param)
    {
        if (param.CompareTo(Min) < 0 || param.CompareTo(Max) > 0)
            throw new InvalidOperationException("invalid argument");
    }

    public T Clamp(T param) => param.CompareTo(Min) < 0 ? Min : param.CompareTo(Max) > 0 ? Max : param;
}

Die Klasse funktioniert für alle Objekte, die es gibt IComparable. Ich erstelle eine Instanz mit einem bestimmten Bereich:

RangeLimit<int> range = new RangeLimit<int>(0, 100);

Ich bestätige entweder ein Argument

range.Validate(value);

oder klemmen Sie das Argument an den Bereich:

var v = range.Validate(value);
Rabbid76
quelle