¿Por qué el valor predeterminado del tipo de cadena es nulo en lugar de una cadena vacía?

218

Es bastante molesto probar todas mis cadenas nullantes de poder aplicar de forma segura métodos como ToUpper(), StartWith()etc.

Si el valor predeterminado de stringfuera la cadena vacía, no tendría que probar, y sentiría que es más consistente con los otros tipos de valores como into doublepor ejemplo. Además Nullable<String>tendría sentido.

Entonces, ¿por qué los diseñadores de C # eligieron usar nullel valor predeterminado de las cadenas?

Nota: Esto se relaciona con esta pregunta , pero se centra más en el por qué en lugar de qué hacer con él.

Marcel
fuente
53
¿Considera que esto es un problema para otros tipos de referencia?
Jon Skeet
17
@ JonSkeet No, pero solo porque inicialmente, erróneamente, pensé que las cadenas son tipos de valor.
Marcel
21
@Marcel: Esa es una buena razón para preguntarse al respecto.
TJ Crowder
77
@ JonSkeet Sí. Oh si. (Pero no eres ajeno a la discusión del tipo de referencia no anulable ...)
Konrad Rudolph
77
Creo que lo pasaría mucho mejor si usara aserciones en sus cadenas en lugares donde espera que NO estén null(y también le recomiendo que trate conceptualmente nully vacíe las cadenas como cosas diferentes). Un valor nulo podría ser el resultado de un error en alguna parte, mientras que una cadena vacía debe transmitir un significado diferente.
diegoreymendez

Respuestas:

312

¿Por qué el valor predeterminado del tipo de cadena es nulo en lugar de una cadena vacía?

Porque stringes un tipo de referencia y el valor predeterminado para todos los tipos de referencia es null.

Es bastante molesto probar nulos todas mis cadenas antes de poder aplicar de forma segura métodos como ToUpper (), StartWith (), etc.

Eso es consistente con el comportamiento de los tipos de referencia. Antes de invocar a los miembros de su instancia, se debe establecer una marca para una referencia nula.

Si el valor predeterminado de la cadena fuera la cadena vacía, no tendría que probar, y sentiría que es más coherente con los otros tipos de valores como int o double, por ejemplo.

Asignar el valor predeterminado a un tipo de referencia específico que nullno sea inconsistente .

Además Nullable<String>tendría sentido.

Nullable<T>trabaja con los tipos de valor. Cabe destacar el hecho de que Nullableno se introdujo en la plataforma .NET original , por lo que habría habido un montón de código roto si hubieran cambiado esa regla. ( Cortesía de @jcolebrand )

Habib
fuente
10
@HenkHolterman One podría implementar un montón de cosas, pero ¿por qué introducir una inconsistencia tan evidente?
44
@delnan - "por qué" fue la pregunta aquí.
Henk Holterman
8
@HenkHolterman Y "Consistencia" es la refutación a su punto "la cadena podría tratarse a diferencia de otros tipos de referencia".
66
@delnan: Al estar trabajando en un lenguaje que trata las cadenas como tipos de valor y trabajando más de 2 años en dotnet, estoy de acuerdo con Henk. Lo veo como un gran defecto en dotnet.
Fabricio Araujo
1
@delnan: se podría crear un tipo de valor que se comportara esencialmente como String, excepto por (1) el comportamiento de tipo de valor de tener un valor predeterminado utilizable, y (2) una desafortunada capa adicional de indirección de boxeo cada vez que se lanzó a Object. Dado que la representación del montón stringes única, tener un tratamiento especial para evitar el boxeo adicional no habría sido muy difícil (en realidad, poder especificar comportamientos de boxeo no predeterminados también sería algo bueno para otros tipos).
supercat
40

Habib tiene razón, porque stringes un tipo de referencia.

Pero lo más importante, no tiene que verificar nullcada vez que lo usa. Sin embargo, probablemente debería arrojar un ArgumentNullExceptionsi alguien pasa su función como nullreferencia.

Aquí está la cosa: el marco arrojaría un NullReferenceExceptionpara usted de todos modos si intenta llamar .ToUpper()a una cadena. Recuerde que este caso aún puede ocurrir incluso si prueba sus argumentos nullya que cualquier propiedad o método en los objetos pasados ​​a su función como parámetros pueden evaluar null.

Dicho esto, la verificación de cadenas vacías o nulos es algo común, por lo que proporcionan String.IsNullOrEmpty()y String.IsNullOrWhiteSpace()para este propósito.

Dave Markle
fuente
30
Nunca debe lanzarse NullReferenceExceptionusted mismo ( msdn.microsoft.com/en-us/library/ms173163.aspx ); arroja un ArgumentNullExceptionsi su método no puede aceptar referencias nulas. Además, los NullRef suelen ser una de las excepciones más difíciles de diagnosticar cuando se solucionan problemas, por lo que no creo que la recomendación de no verificar null sea muy buena.
Andy
3
@Andy "Los NullRef son típicamente una de las excepciones más difíciles de diagnosticar" Estoy totalmente en desacuerdo, si registra cosas es realmente fácil de encontrar y arreglar (solo maneje el caso nulo).
Louis Kottmann
66
Lanzar ArgumentNullExceptiontiene el beneficio adicional de poder proporcionar el nombre del parámetro. Durante la depuración, esto ahorra ... err, segundos. Pero segundos importantes.
Kos
2
@DaveMarkle es posible que desee incluir IsNullOrWhitespace también msdn.microsoft.com/en-us/library/…
Nathan Koop
1
Realmente creo que buscar nulos en todas partes es una fuente de inmensa extensión de código. es feo, se ve hacky y es difícil mantenerse constante. Creo que (al menos en lenguajes similares a C #) una buena regla es "prohibir la palabra clave nula en el código de producción, usarla como loca en el código de prueba".
sara
24

Podría escribir un método de extensión (para lo que vale):

public static string EmptyNull(this string str)
{
    return str ?? "";
}

Ahora esto funciona de manera segura:

string str = null;
string upper = str.EmptyNull().ToUpper();
Tim Schmelter
fuente
100
Pero por favor no lo hagas. Lo último que otro programador quiere ver es miles de líneas de código salpicadas con .EmptyNull () en todas partes solo porque el primer tipo estaba "asustado" de las excepciones.
Dave Markle
15
@DaveMarkle: Pero obviamente es exactamente lo que OP estaba buscando. "Es bastante molesto probar nulas todas mis cadenas antes de poder aplicar de forma segura métodos como ToUpper (), StartWith (), etc."
Tim Schmelter
19
El comentario fue para el OP, no para ti. Si bien su respuesta es claramente correcta, un programador que haga una pregunta básica como esta debería ser fuertemente advertido de no poner su solución en práctica ANCHA, como suele ser su costumbre. Hay una serie de compensaciones que no discute en su respuesta, como opacidad, mayor complejidad, dificultad de refactorización, posible uso excesivo de métodos de extensión y, sí, rendimiento. A veces (muchas veces) una respuesta correcta no es el camino correcto, y es por eso que comenté.
Dave Markle
55
@Andy: La solución para no hacer una verificación nula adecuada es verificar adecuadamente los nulos, no poner una curita en un problema.
Dave Markle
77
Si está pasando por la molestia de escribir .EmptyNull(), ¿por qué no simplemente usarlo (str ?? "")donde sea necesario? Dicho esto, estoy de acuerdo con el sentimiento expresado en el comentario de @ DaveMarkle: probablemente no deberías. nully String.Emptyson conceptualmente diferentes, y no necesariamente puedes tratar a uno igual que a otro.
un CVn
17

También puede usar lo siguiente, a partir de C # 6.0

string myString = null;
string result = myString?.ToUpper();

El resultado de la cadena será nulo.

russelrillema
fuente
1
Para ser correcto, desde c # 6.0, la versión del IDE no tiene nada que ver, ya que esta es una característica del lenguaje.
Stijn Van Antwerpen
3
Otra opción -public string Name { get; set; } = string.Empty;
Jaja Harris
¿Como se llama esto? myString? .ToUpper ();
Hunter Nelson
1
Se llama operador condicional nulo. Puede leer sobre esto aquí msdn.microsoft.com/en-us/magazine/dn802602.aspx
russelrillema
14

Las cadenas vacías y los valores nulos son fundamentalmente diferentes. Un valor nulo es la ausencia de un valor y una cadena vacía es un valor que está vacío.

El lenguaje de programación que hace suposiciones sobre el "valor" de una variable, en este caso una cadena vacía, será tan bueno como iniciar la cadena con cualquier otro valor que no cause un problema de referencia nulo.

Además, si pasa el identificador a esa variable de cadena a otras partes de la aplicación, entonces ese código no tendrá formas de validar si ha pasado intencionalmente un valor en blanco o si ha olvidado completar el valor de esa variable.

Otra ocasión en la que esto sería un problema es cuando la cadena es un valor de retorno de alguna función. Como string es un tipo de referencia y técnicamente puede tener un valor como nulo y vacío, por lo tanto, la función también puede devolver técnicamente un valor nulo o vacío (no hay nada que lo detenga). Ahora, dado que hay 2 nociones de la "ausencia de un valor", es decir, una cadena vacía y un valor nulo, todo el código que consume esta función tendrá que hacer 2 comprobaciones. Uno para vacío y otro para nulo.

En resumen, siempre es bueno tener solo 1 representación para un solo estado. Para una discusión más amplia sobre vacíos y nulos, vea los enlaces a continuación.

/software/32578/sql-empty-string-vs-null-value

NULL vs Empty cuando se trata de la entrada del usuario

Nerrve
fuente
2
¿Y cómo ve exactamente esta diferencia, digamos en un cuadro de texto? ¿Se olvidó el usuario de ingresar un valor en el campo o lo dejaron en blanco a propósito? Nulo en un lenguaje de programación tiene un significado específico; sin asignar Sabemos que no tiene un valor, que no es lo mismo que una base de datos nula.
Andy
1
no hay mucha diferencia cuando lo usas con un cuadro de texto. De cualquier manera, tener una notación para representar la ausencia de un valor en una cadena es primordial. Si tuviera que elegir uno, elegiría nulo.
Nerrve
En Delphi, la cadena es un tipo de valor y, por lo tanto, no puede ser nulo. Hace la vida mucho más fácil a este respecto: realmente me parece muy molesto que la cadena sea un tipo de referencia.
Fabricio Araujo
1
Bajo el COM (Modelo de objetos comunes) que precedió a .net, un tipo de cadena mantendría un puntero a los datos de la cadena o nullrepresentaría la cadena vacía. Hay varias maneras en que .net podría haber implementado una semántica similar, si hubieran elegido hacerlo, especialmente dado que Stringtiene una serie de características que lo convierten en un tipo único de todos modos [por ejemplo, y los dos tipos de matriz son los únicos tipos cuya asignación el tamaño no es constante].
supercat
7

La razón / problema fundamental es que los diseñadores de la especificación CLS (que define cómo interactúan los lenguajes con .net) no definieron un medio por el cual los miembros de la clase pudieran especificar que deben ser llamados directamente, en lugar de vía callvirt, sin que la persona que realiza la llamada realice un verificación de referencia nula; Tampoco proporcionó un significado de estructuras definitorias que no estarían sujetas al boxeo "normal".

Si la especificación CLS hubiera definido dicho medio, entonces sería posible que .net siguiera consistentemente el liderazgo establecido por el Modelo de Objeto Común (COM), bajo el cual una referencia de cadena nula se consideraba semánticamente equivalente a una cadena vacía, y para otros tipos de clase inmutables definidos por el usuario que se supone que tienen una semántica de valores para definir igualmente los valores predeterminados. Esencialmente, lo que sucedería sería que cada miembro de String, por ejemplo, Lengthse escribiera como algo así [InvokableOnNull()] int String Length { get { if (this==null) return 0; else return _Length;} }. Este enfoque habría ofrecido una semántica muy buena para cosas que deberían comportarse como valores, pero debido a problemas de implementación deben almacenarse en el montón. La mayor dificultad con este enfoque es que la semántica de conversión entre estos tipos y Objectpodría volverse un poco turbia.

Un enfoque alternativo habría sido permitir la definición de tipos de estructura especiales que no heredaran, Objectsino que tuvieran operaciones de boxeo y desempaquetado personalizadas (que se convertirían a / de algún otro tipo de clase). Bajo tal enfoque, habría un tipo de clase NullableStringque se comporta como lo hace una cadena ahora, y un tipo de estructura en caja personalizada String, que contendría un único campo Valuede tipo privado String. El intento de convertir un Stringa NullableStringo Objectvolvería Valuesi no nulo, o String.Emptysi es nulo. Intentando convertir String, una referencia no nula a una NullableStringinstancia almacenaría la referencia en Value(tal vez almacenando nulo si la longitud fuera cero); emitir cualquier otra referencia arrojaría una excepción.

Aunque las cadenas deben almacenarse en el montón, conceptualmente no hay ninguna razón por la que no deberían comportarse como tipos de valor que tienen un valor predeterminado no nulo. Tenerlos almacenados como una estructura "normal" que contenía una referencia habría sido eficiente para el código que los usó como tipo "cadena", pero habría agregado una capa adicional de indirección e ineficiencia al convertir a "objeto". Si bien no preveo que .net agregue ninguna de las características anteriores en esta fecha tardía, quizás los diseñadores de futuros marcos podrían considerar incluirlas.

Super gato
fuente
1
Hablando como alguien que trabaja mucho en SQL y ha lidiado con el dolor de cabeza de que Oracle no hace una distinción entre NULL y zero-length, estoy muy contento de que .NET lo haga . "Vacío" es un valor, "nulo" no lo es.
@ JonofAllTrades: No estoy de acuerdo. En el código de la aplicación, excepto en el caso del código db, no tiene sentido que una cadena se trate como una clase. Es un tipo de valor y uno básico. Supercat: +1 para ti
Fabricio Araujo
1
El código de la base de datos es un gran "excepto". Siempre que haya algunos dominios problemáticos en los que necesite distinguir entre "presente / conocido, una cadena vacía" y "no presente / desconocido / inaplicable", como las bases de datos, entonces el idioma debe admitirlo. Por supuesto, ahora que tiene .NET Nullable<>, las cadenas podrían volver a implementarse como tipos de valor; No puedo hablar de los costos y beneficios de tal elección.
3
@JonofAllTrades: el código que trata con números tiene que tener un medio fuera de banda para distinguir el valor predeterminado cero de "indefinido". Tal como está, el código de manejo anulable que funciona con cadenas y números tiene que usar un método para cadenas anulables y otro para números anulables. Incluso si un tipo de clase anulable stringes más eficiente de Nullable<string>lo que sería, tener que usar el método "más eficiente" es más oneroso que poder usar el mismo enfoque para todos los valores de bases de datos anulables.
supercat
5

Porque una variable de cadena es una referencia , no una instancia .

Inicializarlo a Vacío por defecto habría sido posible, pero habría introducido muchas inconsistencias en todo el tablero.

Henk Holterman
fuente
3
No hay una razón particular stringque deba ser un tipo de referencia. Para estar seguros, los caracteres reales que componen la cadena ciertamente deben almacenarse en el montón, pero dada la cantidad de soporte dedicado que las cadenas ya tienen en el CLR, no sería una exageración tener System.Stringun tipo de valor con un Campo privado único Valuede tipo HeapString. Ese campo sería un tipo de referencia, y sería predeterminado null, pero una Stringestructura cuyo Valuecampo era nulo se comportaría como una cadena vacía. La única desventaja de este enfoque sería ...
supercat
1
... que emitir un Stringto a Object, en ausencia de un código de caso especial en el tiempo de ejecución, provocaría la creación de una Stringinstancia en caja en el montón, en lugar de simplemente copiar una referencia al HeapString.
supercat
1
@supercat: nadie dice que la cadena debería / podría ser un tipo de valor.
Henk Holterman
1
Nadie excepto yo. El hecho de que la cadena sea un tipo de valor "especial" (con un campo de tipo de referencia privado) permitiría que la mayor parte del manejo sea esencialmente tan eficiente como lo es ahora, excepto por una comprobación nula adicional de métodos / propiedades como .Lengthetc., de modo que las instancias que se mantienen una referencia nula no intentaría desreferenciarla, sino que se comportaría como corresponde para una cadena vacía. Si el Marco sería mejor o peor con stringimplementada de esa manera, si uno quería default(string)ser una cadena vacía ...
supercat
1
... tener stringun envoltorio de tipo de valor en un campo de tipo de referencia sería el enfoque que requeriría la menor cantidad de cambios a otras partes de .net [de hecho, si uno estuviera dispuesto a aceptar la conversión Stringpara Objectcrear un elemento en caja adicional, uno podría simplemente Stringser una estructura ordinaria con un campo de tipo Char[]que nunca expuso]. Creo que tener un HeapStringtipo probablemente sería mejor, pero de alguna manera la cadena de tipo de valor que contiene un Char[]sería más simple.
supercat
5

¿Por qué los diseñadores de C # optaron por usar nulo como el valor predeterminado de las cadenas?

Como las cadenas son tipos de referencia , los tipos de referencia tienen un valor predeterminado null. Las variables de los tipos de referencia almacenan referencias a los datos reales.

Usemos la defaultpalabra clave para este caso;

string str = default(string); 

stres un string, entonces es un tipo de referencia , entonces el valor predeterminado es null.

int str = (default)(int);

stres un int, entonces es un tipo de valor , entonces el valor predeterminado es zero.

Soner Gönül
fuente
4

Si el valor predeterminado de stringfuera la cadena vacía, no tendría que probar

¡Incorrecto! Cambiar el valor predeterminado no cambia el hecho de que es un tipo de referencia y alguien aún puede establecer explícitamente que la referencia sea null.

Además Nullable<String>tendría sentido.

Punto verdadero Tendría más sentido no permitir nullningún tipo de referencia, sino que requeriría Nullable<TheRefType>esa característica.

Entonces, ¿por qué los diseñadores de C # eligieron usar nullel valor predeterminado de las cadenas?

Consistencia con otros tipos de referencia. Ahora, ¿por qué permitir los nulltipos de referencia? Probablemente para que se sienta como C, a pesar de que esta es una decisión de diseño cuestionable en un lenguaje que también proporciona Nullable.

Dan Burton
fuente
3
¿Podría ser porque Nullable solo se introdujo en .NET 2.0 Framework, por lo que antes no estaba disponible?
jcolebrand
3
Gracias Dan Burton por señalar que alguien PUEDE establecer el valor inicializado como nulo en los tipos de referencia más adelante. Pensar en esto me dice que mi intención original en la pregunta no sirve para nada.
Marcel
4

Quizás si usa el ??operador al asignar su variable de cadena, podría ayudarlo.

string str = SomeMethodThatReturnsaString() ?? "";
// if SomeMethodThatReturnsaString() returns a null value, "" is assigned to str.
Amén Jlili
fuente
2

Una cadena es un objeto inmutable, lo que significa que cuando se le da un valor, el valor anterior no se borra de la memoria, sino que permanece en la ubicación anterior, y el nuevo valor se coloca en una nueva ubicación. Entonces, si el valor predeterminado de String afue String.Empty, desperdiciaría el String.Emptybloque en la memoria cuando se le dio su primer valor.

Aunque parece minúsculo, podría convertirse en un problema al inicializar una gran variedad de cadenas con valores predeterminados de String.Empty. Por supuesto, siempre podría usar la StringBuilderclase mutable si esto fuera un problema.

djv
fuente
Gracias por mencionar la cosa de la "primera inicialización".
Marcel
3
¿Cómo sería un problema al inicializar una gran matriz? Como, como dijiste, las cadenas son inmutables, todos los elementos de la matriz serían simplemente punteros a la misma String.Empty. ¿Estoy equivocado?
Dan Burton
2
El valor predeterminado para cualquier tipo tendrá todos los bits establecidos en cero. La única forma de que el valor predeterminado de stringsea ​​una cadena vacía es permitir que todos los bits cero sean una representación de una cadena vacía. Hay varias maneras en que esto se podría lograr, pero no creo que ninguna implique la inicialización de referencias String.Empty.
supercat
Otras respuestas discutieron este punto también. Creo que la gente ha llegado a la conclusión de que no tendría sentido tratar la clase String como un caso especial y proporcionar algo más que todos los bits cero como inicialización, incluso si fuera algo así como String.Emptyo "".
djv
@DanV: Cambiar el comportamiento de inicialización de stringlas ubicaciones de almacenamiento habría requerido también cambiar el comportamiento de inicialización de todas las estructuras o clases que tienen campos de tipo string. Eso representaría un cambio bastante grande en el diseño de .net, que actualmente espera inicializar a cero cualquier tipo sin siquiera tener que pensar en qué es, salvo solo por su tamaño total.
supercat
2

Como la cadena es un tipo de referencia y el valor predeterminado para el tipo de referencia es nulo.

Akshay
fuente
0

Tal vez la stringpalabra clave lo confundió, ya que se ve exactamente como cualquier otra declaración de tipo de valor , pero en realidad es un alias System.Stringcomo se explica en esta pregunta .
Además, el color azul oscuro en Visual Studio y la primera letra minúscula pueden inducir a error al pensar que es un struct.

Alessandro Da Rugna
fuente
3
¿No es lo mismo cierto para la objectpalabra clave? Aunque es cierto, eso es mucho menos utilizado que string.
2
Como intes un alias para System.Int32. ¿Cual es tu punto? :)
Thorarin
@Thorari @delnan: Los dos son los alias, pero System.Int32es un Structteniendo así un valor por defecto, mientras System.Stringes una Classque tiene un puntero con el valor por defecto de null. Se presentan visualmente en la misma fuente / color. Sin conocimiento, uno puede pensar que actúan de la misma manera (= tener un valor predeterminado). Mi respuesta fue escrita con una idea de psicología cognitiva en.wikipedia.org/wiki/Cognitive_psychology detrás de ella :-)
Alessandro Da Rugna
Estoy bastante seguro de que Anders Hejlsberg lo dijo en una entrevista del canal 9. Sé la diferencia entre el montón y la pila, pero la idea con C # es que el programador casual no necesita hacerlo.
Thomas Koelle
0

Los tipos anulables no llegaron hasta 2.0.

Si los tipos anulables se hubieran hecho al comienzo del lenguaje, entonces string no hubiera sido anulable y string? habría sido anulable. Pero no pudieron hacer esto du a la compatibilidad con versiones anteriores.

Mucha gente habla de ref-type o no ref type, pero string es una clase fuera de lo común y se habrían encontrado soluciones para hacerlo posible.

Thomas Koelle
fuente