Gdzie mogę znaleźć funkcję „clamp” w .NET?

97

Chciałbym ograniczyć wartość xdo zakresu [a, b]:

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

To jest dość podstawowe. Ale nie widzę funkcji „clamp” w bibliotece klas - przynajmniej nie w System.Math.

(Dla nieświadomych „ograniczenia” wartości jest upewnienie się, że mieści się między pewnymi wartościami maksymalnymi i minimalnymi. Jeśli jest większa niż wartość maksymalna, jest zastępowana przez wartość maksymalną itp.)

Danvil
źródło
2
@Danvil: Nie ma „Biblioteki klas C #”. Masz na myśli „.NET Framework”.
John Saunders
1
Wciąż nic od C # 7.1?
joce
1
@JohnSaunders Nie wierzę, że to do końca prawda stackoverflow.com/questions/807880/ ...
Adam Naylor
Gdybym zapytał, jak „ograniczyć” wartość, każdy anglojęzyczny programista na świecie od razu wiedziałby, co mam na myśli. Najprawdopodobniej każdy programista wiedziałby. Po ponad 30 latach w branży musiałem dowiedzieć się, co dziś oznacza „zacisk”. Podobnie jak „wstrzykiwanie zależności” - „parametryzacja” jest czymś tak oczywistym, że nikt nigdy o tym nie napisał.
Bob
@Bob Niektóre słowa mają historyczne, dobrze zdefiniowane znaczenie. Clamp jest jednym z nich. en.wikipedia.org/wiki/Clamping_(graphics) lub khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml lub docs.microsoft.com/en-us/windows/win32/direct3dhlsl/… "Limit „byłoby mylące, zwłaszcza że„ limit ”ma już inne znaczenie w matematyce.
kaalus

Odpowiedzi:

140

Możesz napisać metodę rozszerzenia:

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;
}

Metody rozszerzające znajdują się w klasach statycznych - ponieważ jest to funkcja niskiego poziomu, prawdopodobnie powinna znajdować się w jakiejś podstawowej przestrzeni nazw w Twoim projekcie. Możesz następnie użyć metody w dowolnym pliku kodu, który zawiera dyrektywę using dla przestrzeni nazw, np

using Core.ExtensionMethods

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

.NET Core 2.0

Począwszy od .NET Core 2.0 System.Mathma teraz Clampmetodę, której można użyć zamiast tego:

using System;

int i = Math.Clamp(4, 1, 3);
Zawietrzny
źródło
1
Gdzie powinienem to umieścić i wywołuje CompareTo wolniej niż porównywanie z <(dla typów całkowitych)?
Danvil
1
W klasie statycznej iw środowisku .NET (nie ma pewności co do mono, compact itp.), Typ ogólny powinien zostać ponownie skompilowany dla typu i wbudowany CompareTo, więc nie ma wpływu na wydajność.
Robert Fraser
1
@Frasier O ile nie jest to kod wrażliwy na ultra wydajność, jest mało prawdopodobne, abyś osiągnął w ten sposób znaczący wzrost wydajności. Posiadanie go jako ogólnego jest prawdopodobnie bardziej przydatne niż oszczędność kilku mikrosekund.
MgSam
5
Dobrą rzeczą w ograniczaniu się do ogólnej wersji programu IComparablejest to, że nie występuje boks. To powinno przebiegać bardzo szybko. Pamiętaj, że z doublei float, CompareTometoda odpowiada całkowitej kolejności, gdzie NaNjest mniejsza niż wszystkie inne wartości, w tym NegativeInfinity. Więc to nie jest równoważne z <operatorem. Jeśli używasz <z typem zmiennoprzecinkowym, musisz rozważyć, jak NaNrównież traktować . Nie dotyczy to innych typów liczbowych.
Jeppe Stig Nielsen
1
Należy rozważyć, jak leczyć NaNw obu przypadkach. Wersja z <i >będzie wyjścia NaNi używania NaNna minlub maxskutecznie dokonać jednostronnego zacisku. Dzięki CompareTotemu zawsze wróci, NaNjeśli maxjest NaN.
Herman
32

Po prostu użyj Math.Mini Math.Max:

x = Math.Min(Math.Max(x, a), b);
d7samurai
źródło
To oznacza, że int a0 = x > a ? x : a; return a0 < b ? a0 : b(chociaż daje prawidłowe wyniki) nie jest do końca idealne.
Mr. Smith
12
a czemu to?
d7samurai
4
@ d7samurai Jeśli wiemy, że min <= max, Math.Min(Math.Max(x, min), max)daje o jedno porównanie więcej niż to konieczne, jeśli x <min.
Jim Balter,
@JimBalter, w teorii to prawda. Jeśli spojrzysz na sposób, w jaki zwykle implementuje się CompareTo (), zaakceptowana odpowiedź może zająć do 6 porównań. Nie wiem jednak, czy kompilator jest wystarczająco inteligentny i wstawia metodę CompareTo () i usuwa zbędne porównania.
quinmars
1
Jest to dobre w przypadkach, gdy wystarczy to zrobić tylko raz, a potem zupełnie nowa funkcja wydaje się przesada.
feos
26

Próbować:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}
Clit
źródło
7
Fuj! Te brzydkie, zbędne nawiasy! Jeśli masz zamiar być złym geniuszem z podwójnymi operatorami trójskładnikowymi, przynajmniej zrób to poprawnie i pozbądź się ich również! 😂
XenoRo
9
@XenoRo Te „redundantne” nawiasy sprawiają, że jest czytelny.
Jaśniejsze
2
@Cleaner - 1) Jeśli zależy Ci na czytelności, unikniesz podwójnych ternariów i zamiast tego zostaną użyte bloki IF. 2) Nie rozumiesz żartu, prawda? xD
XenoRo
13

Nie ma jednego, ale nie jest trudno go stworzyć. Znalazłem jeden tutaj: zacisk

To jest:

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;
    }

I może być używany jak:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5
Jeremy B.
źródło
To rozwiązanie jest lepsze niż przyjęte. Żadnej dwuznaczności.
aggsol
6
@CodeClown To rozwiązanie powoduje niepotrzebne porównanie, gdy wartość> max, a odwrócona kolejność argumentów zaprasza (i praktycznie gwarantuje) błędy. Nie wiem, jakiej dwuznaczności według ciebie można uniknąć.
Jim Balter,
Aby zachować spójność ze starszą implementacją Math.Clamp, zalecamy zmianę kolejności parametrów min / max:Clamp(T value, T min, T max)
josh poley
4

Po prostu podzielenie się rozwiązaniem Lee z omówionymi w komentarzach problemami i wątpliwościami, jeśli to możliwe:

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;
}

Różnice:

Ograniczenia: brak zacisków jednostronnych. Jeśli maxtak NaN, zawsze zwraca NaN(zobacz komentarz Hermana ).

XenoRo
źródło
Inne ograniczenie nameofnie działa w języku C # 5 lub starszym.
RoLYroLLs
0

Korzystając z poprzednich odpowiedzi, skondensowałem je do poniższego kodu dla moich potrzeb. Pozwoli to również na ograniczenie liczby tylko przez jej min. Lub maks.

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
źródło
Dlaczego nie return value.ClampedMinimum(min).ClampedMaximum(max);?
Henrik,
0

Poniższy kod obsługuje określanie granic w dowolnej kolejności (tj. bound1 <= bound2Lub bound2 <= bound1). Zauważyłem, że jest to przydatne do mocowania wartości obliczonych z równań liniowych ( y=mx+b), w których nachylenie linii może rosnąć lub maleć.

Wiem: kod składa się z pięciu bardzo brzydkich operatorów wyrażeń warunkowych . Rzecz w tym, że działa , a poniższe testy to potwierdzają. Jeśli sobie tego życzysz, możesz dodać niepotrzebne nawiasy.

Możesz łatwo utworzyć inne przeciążenia dla innych typów liczbowych i po prostu skopiować / wkleić testy.

Ostrzeżenie: porównywanie liczb zmiennoprzecinkowych nie jest proste. Ten kod nie implementuje doublesolidnie porównań. Użyj biblioteki porównawczej zmiennoprzecinkowej, aby zastąpić użycie operatorów porównania.

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;
    }
}

Testy xUnit / FluentAssertions:

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
źródło
0

Jeśli chcę sprawdzić zakres argumentu w [min, max], używam następującej przydatnej klasy:

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;
}

Klasa działa dla wszystkich obiektów, które są IComparable. Tworzę instancję o określonym zakresie:

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

Albo potwierdzam argument

range.Validate(value);

lub ogranicz argument do zakresu:

var v = range.Validate(value);
Rabbid76
źródło