¿Dónde puedo encontrar la función "sujetar" en .NET?

95

Me gustaría fijar un valor xa un rango [a, b]:

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

Esto es bastante básico. Pero no veo una función "clamp" en la biblioteca de clases, al menos no en System.Math.

(Para los que no lo saben, "sujetar" un valor es asegurarse de que se encuentra entre algunos valores máximos y mínimos. Si es mayor que el valor máximo, entonces se reemplaza por el máximo, etc.)

Danvil
fuente
2
@Danvil: No hay una "Biblioteca de clases C #". Te refieres a "El .NET Framework".
John Saunders
1
¿Todavía no hay nada a partir de C # 7.1?
joce
1
@JohnSaunders No creo que eso sea estrictamente cierto stackoverflow.com/questions/807880/…
Adam Naylor
Si preguntara cómo "limitar" un valor, todos los programadores de habla inglesa del mundo sabrían inmediatamente lo que quiero decir. Lo más probable es que todos los programadores lo sepan. Después de más de 30 años en el negocio, tuve que averiguar qué significaba "abrazadera" hoy. Similar a la "inyección de dependencia", la "parametrización" es algo tan obvio que nadie ha escrito un libro sobre ello.
Bob
@Bob Algunas palabras tienen un significado histórico bien definido. Clamp es uno de ellos. en.wikipedia.org/wiki/Clamping_(graphics) o khronos.org/registry/OpenGL-Refpages/gl4/html/clamp.xhtml o docs.microsoft.com/en-us/windows/win32/direct3dhlsl/… "Límite "sería engañoso, especialmente porque" límite "ya tiene un significado diferente en matemáticas.
kaalus

Respuestas:

137

Podrías escribir un método de extensión:

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

Los métodos de extensión van en clases estáticas; dado que esta es una función de nivel bastante bajo, probablemente debería ir en algún espacio de nombres central en su proyecto. Luego puede usar el método en cualquier archivo de código que contenga una directiva using para el espacio de nombres, por ejemplo

using Core.ExtensionMethods

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

.NET Core 2.0

Comenzando con .NET Core 2.0 System.Mathahora tiene un Clampmétodo que se puede usar en su lugar:

using System;

int i = Math.Clamp(4, 1, 3);
Sotavento
fuente
1
¿Dónde pondría esto y llamar a CompareTo más lento que comparar con <(para tipos integrales)?
Danvil
1
En una clase estática, y en el marco .NET (no estoy seguro de mono, compacto, etc.), el genérico debe recompilarse para el tipo y CompareTo en línea, por lo que no hay penalización de rendimiento.
Robert Fraser
1
@Frasier A menos que se trate de un código muy sensible al rendimiento, es poco probable que obtenga ganancias significativas de rendimiento al hacerlo. Hacer que sea genérico probablemente sea más útil que ahorrar unos pocos microsegundos.
MgSam
4
Lo bueno de limitarse a la versión genérica de IComparablees que no se produce boxeo. Esto debería funcionar muy rápido. Recuerde que con doubley float, el CompareTométodo corresponde a un orden total donde NaNes menor que todos los demás valores, incluidos NegativeInfinity. Entonces no es equivalente al <operador. Si lo usó <con un tipo de punto flotante, también tendría que considerar cómo tratar NaN. Esto no es relevante para otros tipos numéricos.
Jeppe Stig Nielsen
1
Debería considerar cómo tratar NaNen cualquier caso. La versión con <y >haría de salida NaNy el uso NaNde mino maxva en detrimento de una pinza de un solo lado. Con CompareToella siempre volvería NaNsi maxes NaN.
Herman
30

Solo usa Math.Miny Math.Max:

x = Math.Min(Math.Max(x, a), b);
d7samurai
fuente
Eso se traduce en int a0 = x > a ? x : a; return a0 < b ? a0 : bque (aunque da resultados correctos) no es exactamente lo ideal.
Mr. Smith
12
¿Y por qué es eso?
d7samurai
4
@ d7samurai Si sabemos que min <= max, Math.Min(Math.Max(x, min), max)resulta en una comparación más de la necesaria si x <min.
Jim Balter
@JimBalter, en teoría esto es cierto. Si observa cómo se implementa típicamente CompareTo (), la respuesta aceptada puede requerir hasta 6 comparaciones. Sin embargo, no sé si el compilador es lo suficientemente inteligente e integra CompareTo () y elimina las comparaciones superfluas.
quinmars
1
Esto es bueno para los casos en los que solo necesita hacerlo una vez, luego una función completamente nueva para eso se siente como una exageración.
feos
26

Tratar:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}
Clítoris
fuente
6
¡Uf! ¡Esos feos paréntesis redundantes! Si vas a ser un genio malvado con los operadores ternarios dobles, ¡al menos hazlo correctamente y deshazte de ellos también! 😂
XenoRo
8
@XenoRo Esos paréntesis "redundantes" son los que lo hacen legible.
Más claro
2
@Cleaner - 1) Si busca legibilidad, se evitarían ternarios dobles y en su lugar se usarían bloques IF. 2) No entiendes la broma, ¿verdad? xD
XenoRo
13

No hay uno, pero no es demasiado difícil hacer uno. Encontré uno aquí: abrazadera

Es:

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

Y se puede usar como:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5
Jeremy B.
fuente
Esta solución es mejor que la aceptada. Sin ambigüedad.
aggsol
6
@CodeClown Esta solución da como resultado una comparación innecesaria cuando value> max, y el orden de argumento invertido invita (y virtualmente garantiza) errores. No sé qué ambigüedad cree que se evita.
Jim Balter
Para mantener la coherencia con la implementación heredada de Math.Clamp, recomendamos cambiar el orden de los parámetros min / max:Clamp(T value, T min, T max)
josh poley
4

Simplemente compartiendo la solución de Lee con los problemas e inquietudes de los comentarios abordados, cuando sea posible:

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

Diferencias:

Limitaciones: No se permiten abrazaderas unilaterales. Si maxes NaN, siempre regresa NaN(Ver comentario de Herman ).

XenoRo
fuente
Otra limitación es nameofque no funciona para C # 5 o inferior.
RoLYroLLs
0

Usando las respuestas anteriores, lo condensé en el siguiente código para mis necesidades. Esto también le permitirá fijar un número solo por su mínimo o máximo.

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
fuente
¿Por qué no return value.ClampedMinimum(min).ClampedMaximum(max);?
Henrik
0

El siguiente código admite la especificación de límites en cualquier orden (es decir bound1 <= bound2, o bound2 <= bound1). Encontré esto útil para fijar valores calculados a partir de ecuaciones lineales ( y=mx+b) donde la pendiente de la línea puede aumentar o disminuir.

Lo sé: el código consta de cinco operadores de expresión condicional súper feos . La cosa es que funciona , y las pruebas a continuación lo demuestran. No dude en agregar paréntesis estrictamente innecesarios si así lo desea.

Puede crear fácilmente otras sobrecargas para otros tipos numéricos y básicamente copiar / pegar las pruebas.

Advertencia: comparar números de punto flotante no es sencillo. Este código no implementa doublecomparaciones de manera sólida. Utilice una biblioteca de comparación de punto flotante para reemplazar los usos de los operadores de comparación.

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

Pruebas de 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
fuente
0

Si quiero validar el rango de un argumento en [min, max], uso la siguiente clase útil:

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

La clase funciona para todos los objetos que son IComparable. Creo una instancia con un cierto rango:

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

Yo valido un argumento

range.Validate(value);

o sujete el argumento al rango:

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