Dove posso trovare la funzione "clamp" in .NET?


92

Vorrei bloccare un valore xsu un intervallo [a, b]:

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

Questo è abbastanza semplice. Ma non vedo una funzione "clamp" nella libreria di classi - almeno non in System.Math.

(Per chi non è consapevole di "bloccare" un valore è assicurarsi che si trovi tra alcuni valori massimo e minimo. Se è maggiore del valore massimo, viene sostituito dal massimo, ecc.)


2
@ Danvil: non esiste una "Libreria di classi C #". Vuoi dire "The .NET Framework".
John Saunders

1
Ancora niente a partire da C # 7.1?
giovedì

1
@JohnSaunders Non credo che sia strettamente vero stackoverflow.com/questions/807880/…
Adam Naylor

Se chiedessi come "limitare" un valore, ogni singolo programmatore di lingua inglese nel mondo saprebbe immediatamente cosa intendo. Molto probabilmente ogni programmatore lo saprebbe. Dopo più di 30 anni di attività ho dovuto scoprire cosa significava oggi "morsetto". Simile all '"iniezione di dipendenza" - la "parametrizzazione" è una cosa così ovvia che nessuno ha mai scritto un libro su di essa.
Bob il

@Bob Alcune parole hanno un significato storico e ben definito. Il morsetto è uno di loro. 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/… "Limit "sarebbe fuorviante, soprattutto che" limite "ha già un significato diverso in matematica.
kaalus

Risposte:


135

Potresti scrivere un metodo di estensione:

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

I metodi di estensione vanno in classi statiche: poiché si tratta di una funzione di livello piuttosto basso, probabilmente dovrebbe andare in uno spazio dei nomi di base nel progetto. È quindi possibile utilizzare il metodo in qualsiasi file di codice che contiene una direttiva using per lo spazio dei nomi, ad es

using Core.ExtensionMethods

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

.NET Core 2.0

A partire da .NET Core 2.0 System.Mathora ha un fileClamp metodo che può essere utilizzato al suo posto:

using System;

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

1
Dove lo metterei e sta chiamando CompareTo più lentamente rispetto al confronto con <(per i tipi integrali)?
Danvil

1
In una classe statica e nel framework .NET (non sono sicuro di mono, compact, ecc.), Il generico dovrebbe essere ricompilato per il tipo e CompareTo inline, quindi nessuna penalizzazione delle prestazioni.
Robert Fraser

1
@Frasier A meno che non si tratti di codice estremamente sensibile alle prestazioni, è improbabile che in questo modo si ottengano miglioramenti significativi delle prestazioni. Averlo generico è probabilmente più utile che risparmiare pochi microsecondi.
MgSam

4
La cosa buona del vincolare alla versione generica di IComparableè che non si verifica la boxe. Questo dovrebbe funzionare molto velocemente. Ricorda che con doublee float, il CompareTometodo corrisponde a un ordine totale in cui NaNè inferiore a tutti gli altri valori, incluso NegativeInfinity. Quindi non è equivalente <all'operatore. Se lo usi <con un tipo a virgola mobile, dovresti considerare anche come trattare NaN. Questo non è rilevante per altri tipi numerici.
Jeppe Stig Nielsen

1
Dovresti considerare come trattare NaNin entrambi i casi. La versione con <e >sarebbe prodotta NaNe utilizzando NaNper mino maxfarebbe effettivamente un morsetto unilaterale. Con CompareToesso tornerebbe sempre NaNse lo maxè NaN.
Herman

29

Basta usare Math.Mine Math.Max:

x = Math.Min(Math.Max(x, a), b);

Ciò significa int a0 = x > a ? x : a; return a0 < b ? a0 : bche (sebbene dia risultati corretti) non è esattamente l'ideale.
Mr. Smith

12
e perché è così?
d7samurai

4
@ d7samurai Se sappiamo che min <= max, Math.Min(Math.Max(x, min), max)risulta in un confronto in più del necessario se x <min.
Jim Balter

@ JimBalter, in teoria questo è vero. Se guardi come viene implementato in genere CompareTo (), la risposta accettata può richiedere fino a 6 confronti. Non so, però, se il compilatore è abbastanza intelligente e integra CompareTo () e rimuove i confronti superflui.
quinmars

1
Questo è un bene per i casi in cui devi farlo solo una volta, quindi una funzione completamente nuova per quella sembra eccessiva.
feos

26

Provare:

public static int Clamp(int value, int min, int max)  
{  
    return (value < min) ? min : (value > max) ? max : value;  
}

6
Uffa! Quelle brutte parentesi ridondanti! Se vuoi essere un genio del male con i doppi operatori ternari, fallo almeno correttamente e sbarazzati anche di quelli! 😂
XenoRo

8
@XenoRo Quelle parentesi "ridondanti" sono ciò che lo rende leggibile.
Più sereno

2
@Cleaner - 1) Se stai cercando la leggibilità, i doppi ternari sarebbero evitati e invece verrebbero usati i blocchi IF. 2) Non hai capito lo scherzo, vero? xD
XenoRo

13

Non ce n'è uno, ma non è troppo difficile crearne uno. Ne ho trovato uno qui: clamp

È:

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

E può essere utilizzato come:

int i = Clamp(12, 10, 0); -> i == 10
double d = Clamp(4.5, 10.0, 0.0); -> d == 4.5

Questa soluzione è migliore di quella accettata. Nessuna ambiguità.
aggsol

6
@CodeClown Questa soluzione si traduce in un confronto non necessario quando value> max e l'ordine invertito degli argomenti invita (e virtualmente garantisce) bug. Non so quale ambiguità pensi venga evitata.
Jim Balter

Per coerenza con l'implementazione legacy di Math.Clamp, consigliamo di cambiare l'ordine dei parametri min / max:Clamp(T value, T min, T max)
josh poley


4

Basta condividere la soluzione di Lee con i problemi dei commenti e le preoccupazioni affrontate, ove possibile:

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

Differenze:

Limitazioni: nessun morsetto unilaterale. Se maxè NaN, restituisce sempre NaN(vedere il commento di Herman ).


Un'altra limitazione è nameofche non funziona per C # 5 o versioni precedenti.
RoLYroLLs

0

Utilizzando le risposte precedenti, l'ho condensato nel codice seguente per le mie esigenze. Ciò consentirà anche di bloccare un numero solo in base al suo minimo o massimo.

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

Perché no return value.ClampedMinimum(min).ClampedMaximum(max);?
Henrik

0

Il codice seguente supporta la specifica dei limiti in qualsiasi ordine (ad esempio bound1 <= bound2, o bound2 <= bound1). L'ho trovato utile per i valori di bloccaggio calcolati da equazioni lineari (y=mx+b ) in cui la pendenza della linea può essere crescente o decrescente.

Lo so: il codice è composto da cinque terribili operatori di espressioni condizionali . Il fatto è che funziona e i test seguenti lo dimostrano. Sentiti libero di aggiungere parentesi strettamente inutili se lo desideri.

Puoi facilmente creare altri overload per altri tipi numerici e sostanzialmente copiare / incollare i test.

Avvertenza: confrontare i numeri in virgola mobile non è semplice. Questo codice non implementa i doubleconfronti in modo affidabile. Utilizzare una libreria di confronto in virgola mobile per sostituire gli usi degli operatori di confronto.

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 test:

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

0

Se voglio convalidare l'intervallo di un argomento in [min, max], utilizzo la seguente pratica classe:

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 classe funziona per tutti gli oggetti che sono IComparable. Creo un'istanza con un certo intervallo:

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

Convalido un argomento

range.Validate(value);

o blocca l'argomento all'intervallo:

var v = range.Validate(value);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.