Pasar parámetros por valor si. Pasar parámetros por referencia y por valor. Pasar parámetros por valor y por referencia

Los parámetros se pueden pasar a una función de una de las siguientes maneras:

Al pasar argumentos por valor, el compilador crea una copia temporal del objeto a pasar y lo coloca en el área de memoria de pila dedicada a almacenar objetos locales. La función llamada opera sobre esta copia, sin afectar el objeto original. Los prototipos de funciones que toman argumentos por valor proporcionan el tipo de objeto como parámetro, no su dirección. Por ejemplo, la función

int ObtenerMax(int, int);

toma dos argumentos enteros por valor.

Si es necesario que la función modifique el objeto original, se utiliza el paso de parámetros por referencia. En este caso, no se pasa el objeto en sí a la función, sino solo su dirección. Por lo tanto, todas las modificaciones en el cuerpo de la función de los argumentos que se le pasan por referencia afectan al objeto. Teniendo en cuenta el hecho de que una función solo puede devolver un único valor, pasar la dirección de un objeto es una forma muy eficiente de trabajar con grandes cantidades de datos. Además, dado que se pasa la dirección y no el objeto en sí, la memoria de pila se ahorra significativamente.

Con la ayuda de punteros.

La sintaxis de referencia implica el uso de una referencia a un tipo de objeto como argumento. Por ejemplo, la función

Pegamento doble (largo y var1, int y var2);

recibe dos referencias a variables de tipo long e int. Al pasar un parámetro de referencia a una función, el compilador pasa automáticamente la dirección de la variable especificada como argumento a la función. No es necesario poner un ampersand antes de un argumento en una llamada de función. Por ejemplo, para la función anterior, una llamada con paso de parámetros por referencia se ve así:

pegamento (var1, var2);

A continuación se muestra un ejemplo de un prototipo de función al pasar parámetros a través de un puntero:

void SetNumber(int*, long*);

Además, las funciones pueden devolver no solo el valor de alguna variable, sino también un puntero o una referencia a ella. Por ejemplo, funciones cuyo prototipo es:

*int Cuenta(int); &int Incremento();

devuelve un puntero y una referencia, respectivamente, a una variable entera de tipo int. Tenga en cuenta que devolver una referencia o un puntero desde una función puede generar problemas si la variable a la que se hace referencia está fuera del alcance. Por ejemplo,

La eficiencia de pasar la dirección de un objeto en lugar de la variable misma también se nota en la velocidad, especialmente si se usan objetos grandes, en particular arreglos (que se discutirán más adelante).

Si se requiere pasar un objeto bastante grande a la función, pero no se proporciona su modificación, en la práctica, se usa pasar un puntero constante. Este tipo de llamada implica el uso de la palabra clave const, por ejemplo, la función

const int* FName(int* const Número)

acepta y devuelve un puntero a un objeto const de tipo int. Cualquier intento de modificar dicho objeto dentro del cuerpo de la función llamada generará un mensaje de error del compilador. Veamos un ejemplo que ilustra el uso de punteros constantes.

#incluir

int* const call(int* const);

int X = 13; int*pX = llamar(pX);

llamada int* const(int* const x)

//*x++; ¡II no puedes modificar el objeto! devolver x;

En lugar de la sintaxis de puntero constante anterior, también puede usar referencias constantes al pasar parámetros, así:

const int& FName (const int& Número)

teniendo el mismo significado que punteros constantes.

#incluir

const int& call(const int&x)

// ¡no puedes modificar el objeto!

Me disculpo de antemano por la anotación pretenciosa sobre la "ubicación de los puntos", pero de alguna manera debo atraerlo al artículo)) Por mi parte, intentaré que la anotación cumpla con sus expectativas.

Brevemente de que se trata

Todo el mundo ya sabe esto, pero sin embargo, al principio, permítame recordarle cómo se pueden pasar los parámetros del método en 1C. Se pueden pasar por referencia o por valor. En el primer caso, le pasamos al método el mismo valor que en el punto de llamada, y en el segundo, una copia del mismo.

De forma predeterminada, en 1C, los argumentos se pasan por referencia y cambiar el parámetro dentro del método será visible desde fuera del método. Aquí, una mayor comprensión del problema depende de qué quiere decir exactamente con la palabra "cambio de parámetro". Entonces, significa reasignación y nada más. Además, la asignación puede ser implícita, por ejemplo, llamando a un método de plataforma que devuelve algo en el parámetro de salida.

Pero si no queremos que nuestro parámetro se pase por referencia, entonces podemos prefijar el parámetro con la palabra clave Valor

Procedimiento por valor (parámetro de valor) Parámetro = 2; Parámetro de procedimiento final = 1; Por valor (parámetro); Notificar (Parámetro); // imprime 1

Todo funciona como se prometió: cambiar (o más bien "reemplazar") el valor del parámetro no cambia el valor fuera del método.

Bueno, ¿cuál es la diversión?

Los momentos interesantes comienzan cuando comenzamos a pasar como parámetros no tipos primitivos (cadenas, números, fechas, etc.), sino objetos. Aquí es donde surgen conceptos como copia "superficial" y "profunda" de un objeto, así como punteros (no en términos de C ++, sino como identificadores abstractos).

Al pasar un objeto (por ejemplo, una tabla de valores) por referencia, pasamos el valor del puntero en sí (un identificador determinado), que "mantiene" el objeto en la memoria de la plataforma. Cuando se pasa por valor, el marco hará una copia de este puntero.

En otras palabras, si, pasando un objeto por referencia, en el método asignamos el valor "Array" al parámetro, entonces obtendremos una matriz en el punto de llamada. La reasignación de un valor pasado por referencia es visible desde el sitio de la llamada.

Procedimiento ProcessValue(Parámetro) Parámetro = Nueva matriz; EndProcedure Table = New ValueTable; procesoValor(tabla); Informe(TipoZnch(Tabla)); // generará una matriz

Sin embargo, si pasamos el objeto por valor, entonces nuestra tabla de valores no se perderá en el punto de llamada.

Contenido y estado del objeto

Al pasar por valor, no se copia todo el objeto, sino solo su puntero. La instancia del objeto sigue siendo la misma. No importa cómo pase el objeto, por referencia o por valor: borrar la tabla de valores borrará la tabla misma. Esta limpieza será visible en todas partes, ya que el objeto era el único e independientemente de cómo se pasara al método.

Procedimiento ProcessValue(Parámetro) Parámetro.Clear(); EndProcedure Table = New ValueTable; Tabla.Add(); procesoValor(tabla); Notificar(Tabla. Cantidad()); // imprime 0

Al pasar objetos a métodos, la plataforma opera con punteros (condicionales, no análogos directos de C ++). Si un objeto se pasa por referencia, entonces la ubicación de la memoria máquina virtual 1C, en el que se encuentra este objeto, puede ser sobrescrito por otro objeto. Si el objeto se pasa por valor, el puntero se copia y al sobrescribir el objeto no se sobrescribe la ubicación de la memoria con el objeto original.

Al mismo tiempo, cualquier cambio estados objeto (borrar, agregar propiedades, etc.) cambia el objeto en sí mismo y, por lo general, no tiene nada que ver con cómo y dónde se transfirió el objeto. El estado de una instancia de objeto ha cambiado, puede haber un montón de "referencias secundarias" y "valores secundarios", pero la instancia es siempre la misma. Al pasar un objeto a un método, no creamos una copia del objeto completo.

Y eso siempre es cierto, excepto...

Interacción cliente-servidor

La plataforma implementa las llamadas al servidor de manera muy transparente. Simplemente llamamos al método y, bajo el capó, la plataforma serializa (lo convierte en una cadena) todos los parámetros del método, los pasa al servidor y luego devuelve los parámetros de salida al cliente, donde se deserializan y viven como si nunca hubieran estado en ningún servidor.

Como sabe, no todos los objetos de la plataforma son serializables. Aquí es donde crece la limitación de que no todos los objetos se pueden pasar al método del servidor desde el cliente. Si pasa un objeto no serializable, la plataforma comenzará a maldecir.

  • Una declaración explícita de la intención del programador. Al observar la firma de un método, puede saber claramente qué parámetros son de entrada y cuáles son de salida. Este código es más fácil de leer y mantener.
  • Para que un cambio en el servidor del parámetro "por referencia" sea visible en el punto de llamada del cliente, parámetros pasados ​​al servidor por referencia, la plataforma necesariamente volverá al propio cliente para garantizar el comportamiento descrito al principio del artículo. Si no es necesario devolver el parámetro, habrá un exceso de tráfico. Para optimizar el intercambio de datos, los parámetros cuyos valores no necesitamos en la salida deben marcarse con la palabra Valor.

Aquí llama la atención el segundo punto. Para optimizar el tráfico, la plataforma no devolverá el valor del parámetro al cliente si el parámetro está marcado con la palabra Valor. Todo esto es maravilloso, pero conduce a un efecto interesante.

Como dije, cuando un objeto se transfiere al servidor, se produce la serialización, es decir. se realiza una copia "profunda" del objeto. Y si hay una palabra Valor el objeto no viajará del servidor al cliente. Sumando estos dos hechos, obtenemos lo siguiente:

&AtServer Procedimiento ByReference(Parameter) Parameter.Clear(); EndProcedure &AtServer Procedimiento por valor (parámetro de valor) Parameter.Clear(); EndProcedure &AtClient Procedimiento por ValueClient(Parámetro de valor) Parameter.Clear(); EndProcedure &AtClient Procedimiento CheckValue() List1= New ValueList; List1.Add("hola"); Lista2 = Lista1.Copiar(); Lista3 = Lista1.Copiar(); // el objeto se copia por completo, // se transfiere al servidor y luego se devuelve. // borrar la lista es visible en el punto de llamada ByReference(List1); // el objeto se copia completamente, // se transfiere al servidor. no vuelve // Limpiar la lista NO ES VISIBLE en el punto de llamada By Value(List2); // solo se copia el puntero del objeto // el borrado de la lista es visible en el punto de llamada ByValueClient(List3); Notificar(Lista1.Cantidad()); Notificar(Lista2.Cantidad()); Notificar(Lista3.Cantidad()); Procedimiento final

Resumen

En resumen, se puede resumir de la siguiente manera:

  • Pasar por referencia le permite "sobrescribir" un objeto con un objeto completamente diferente
  • Pasar por valor no le permite "sobrescribir" el objeto, pero los cambios en el estado interno del objeto serán visibles, porque trabajar con la misma instancia de objeto
  • En una llamada del servidor, el trabajo va con DIFERENTES instancias del objeto, porque Se realizó una copia profunda. Palabra clave Valor prohibirá volver a copiar la instancia del servidor en el cliente, y cambiar el estado interno del objeto en el servidor no conducirá a un cambio similar en el cliente.

Espero que esta simple lista de reglas le facilite la resolución de disputas con colegas sobre el paso de parámetros "por valor" y "por referencia".

Cuando comencé a programar en C ++ y estudié libros y artículos intensamente, invariablemente me encontré con el mismo consejo: si necesitamos pasar algún objeto a una función que no debe cambiarse en la función, entonces siempre debe pasarse por referencia a una constante(PPSC), excepto en aquellos casos en los que necesitemos pasar un tipo primitivo o una estructura de tamaño similar a ellos. Porque durante más de 10 años de programación en C ++, muy a menudo me encontré con este consejo (y me lo di más de una vez), me "absorbió" hace mucho tiempo: automáticamente paso todos los argumentos por referencia a un constante. Pero el tiempo pasa y han pasado 7 años desde que tenemos C++ 11 con su semántica de movimiento, en relación con lo cual escucho más y más voces que cuestionan el buen viejo dogma. Muchos comienzan a argumentar que pasar por referencia a una constante es el siglo pasado y ahora necesita pasar por valor(PPZ). Qué hay detrás de estas conversaciones, así como qué conclusiones podemos sacar de todo esto, quiero discutir en este artículo.

sabiduría del libro

Para entender a qué regla debemos adherirnos, propongo recurrir a los libros. Los libros son una gran fuente de información que no tenemos que aceptar, pero definitivamente vale la pena escucharlos. Empecemos con la historia, desde el principio. No voy a averiguar quién fue el primer apologista de CCPP, solo citaré como ejemplo el libro que personalmente me influenció más en el uso de CCPP.

Meyers

Bien, aquí tenemos una clase en la que todos los parámetros se pasan por referencia, ¿hay algún problema con esta clase? Desafortunadamente, existe, y este problema se encuentra en la superficie. Tenemos funcionalmente 2 entidades en la clase: la primera toma un valor en la etapa de creación de un objeto, y la segunda le permite cambiar el valor establecido previamente. Tenemos dos entidades, pero cuatro funciones. Ahora imagine que podemos tener no 2 entidades similares, sino 3, 5, 6, ¿entonces qué? Entonces estamos esperando una gran cantidad de código fuerte. Por lo tanto, para no producir una masa de funciones, apareció una propuesta para rechazar las referencias en los parámetros en general:

Modelo Titular de la clase ( público: explícito Titular (valor T): m_Value (mover (valor)) ( ) void setValue (T value) ( ​​m_Value = mover (valor); ) const T& value() const noexcept ( return m_Value; ) privado: Tm_Value;);

La primera ventaja que llama la atención de inmediato es que hay mucho menos código. Es incluso más pequeño que en la primera versión, debido a la eliminación de const y & (sin embargo, agregaron move). ¡Pero siempre nos han enseñado que pasar por referencia tiene más rendimiento que pasar por valor! Así era antes de C++11, así es todavía, pero ahora, si miramos este código, veremos que aquí no hay más copias que en la primera versión, siempre que T tenga un constructor de movimiento. Aquellos. por sí mismo, PPSC fue y será más rápido que PPZ, pero el código de alguna manera usa la referencia pasada y, a menudo, este argumento se copia.

Sin embargo, esta no es toda la historia. A diferencia de la primera opción, donde solo tenemos copiar, aquí también se agrega mover. Pero mudarse es una operación barata, ¿verdad? Sobre este tema, el libro de Mayers que estamos considerando también tiene un capítulo ("Artículo 29"), que se titula: "Suponga que las operaciones de movimiento no están presentes, no son baratas y no se usan". La idea principal debe quedar clara en el título, pero si quieres detalles, entonces échale un vistazo, no me detendré en eso.

Aquí sería apropiado realizar un análisis comparativo completo del primer y último método, pero no quisiera desviarme del libro, así que dejaremos el análisis para otras secciones, y aquí continuaremos considerando los argumentos de Scott. Entonces, aparte del hecho de que la tercera opción es obviamente más corta que la segunda, ¿dónde ve Scott la ventaja de PPP sobre PPSC en el código actual?

Lo ve en el hecho de que en el caso de pasar un rvalue, i.e. alguna llamada de este tipo: Holder holder(string("me")); , la opción PPSC nos dará una copia, y la opción PPP nos dará un movimiento. Por otro lado, si el pase es así: Holder holder(someLvalue); , entonces el PPZ definitivamente pierde debido al hecho de que realizará copias y movimientos, mientras que en la variante con PPSK solo habrá una copia. Aquellos. Resulta que el PPP, si consideramos puramente la eficiencia, es un compromiso entre la cantidad de código y el soporte "completo" (a través de && ) para la semántica de movimiento.

Es por eso que Scott ha formulado su consejo con tanto cuidado y lo promueve con tanto cuidado. Incluso me pareció que lo citaba de mala gana, como bajo presión: fue ampliamente discutido, y Scott siempre ha sido un coleccionista de experiencia colectiva. Además, cita muy pocos argumentos en defensa del PPP, pero cita muchos de los que cuestionan esta “técnica”. Exploraremos sus contras en secciones posteriores, pero aquí reiteraremos brevemente el argumento de Scott en defensa del PPP (agregando pensativamente "si el objeto soporta movimiento y es barato"): evita la copia al pasar una expresión rvalue como argumento de función. Pero basta de torturar el libro de Meyers, pasemos a otro libro.

Por cierto, si alguien ha leído el libro y se sorprende de que no incluya aquí una variante de lo que Meyers llamó referencias universales, ahora conocidas como referencias de reenvío, esto se explica fácilmente. Solo estoy considerando PPP y PPSP, como para introducir funciones de plantilla para métodos que no son plantillas, solo por admitir el paso por referencia de ambos tipos (rvalue / lvalue) lo considero de mala forma. Eso sin contar que el código resulta ser diferente (ya no hay constancia) y trae consigo otros problemas.

Josatis y compañia

El último libro que veremos es Plantillas de C++, que también es el más reciente de todos los libros mencionados en este artículo. Salió a fines de 2017 (y dentro del libro, en general, se indica 2018). A diferencia de otros libros, este se trata de patrones, no de consejos (como Meyers) o C++ en general, como Stroustrup. Por lo tanto, aquí se consideran los pros/contras desde el punto de vista de las plantillas de escritura.

A este tema se dedica todo un capítulo 7, que lleva el elocuente título “¿Por valor o por referencia?”. En este capítulo, los autores describen de manera bastante breve pero amplia todos los métodos de transmisión con todos sus pros y sus contras. El análisis de eficiencia prácticamente no se da aquí, y se da por sentado que PPSC será más rápido que PPZ. Pero con todo esto, al final del capítulo, los autores recomiendan usar el PPP para funciones de plantilla por defecto. ¿Por qué? Porque al usar una referencia, los parámetros de la plantilla se muestran completos, y sin una referencia se "descomponen" (decaen), lo que afecta favorablemente el procesamiento de matrices y cadenas literales. Los autores creen que si para algún tipo de PPP resulta ser ineficiente, siempre se puede usar std::ref y std::cref . Tal consejo, para ser honesto, ¿has visto a muchos que quieren usar las funciones anteriores?

¿Qué aconsejan sobre PPSC? Aconsejan usar PPSC cuando el rendimiento es crítico o hay otros pesado razones para no utilizar el PPP. Por supuesto, aquí solo estamos hablando de código repetitivo, pero este consejo contradice directamente todo lo que se les ha enseñado a los programadores durante una década. Este no es solo un consejo para considerar PPP como una alternativa, no, es un consejo para hacer de KPP una alternativa.

Esto concluye nuestra gira de libros, porque. No conozco ningún otro libro que debamos leer sobre este tema. Pasemos a otro espacio mediático.

Sabiduría de la red

Porque Vivimos en la era de Internet, entonces uno no debe confiar en la sabiduría de los libros. Además, muchos autores que solían escribir libros ahora solo escriben blogs, y los libros han sido abandonados. Uno de estos autores es Herb Sutter, quien en mayo de 2013 publicó un artículo en su blog "GotW #4 Solution: Class Mechanics", que, aunque no se dedica por completo al problema que abordamos, sí lo toca.

Entonces, en la versión original del artículo, Sutter simplemente repitió la vieja sabiduría: "pasar parámetros por referencia a una constante", pero ya no veremos esta versión del artículo, porque. El artículo contiene el consejo opuesto: si el parámetro aún se copiará, luego páselo por valor. De nuevo el notorio "si". ¿Por qué Sutter cambió el artículo y cómo lo supe? De los comentarios. Lea los comentarios a su artículo, por cierto, son más interesantes y más útiles que el artículo en sí. Es cierto que después de escribir el artículo, Sutter, sin embargo, cambió de opinión y ya no da ese consejo. El cambio de opinión se puede ver en su charla CppCon 2014: “¡Regreso a lo básico! Fundamentos del estilo C++ moderno". Asegúrese de comprobarlo, pasaremos al siguiente enlace de Internet.

Y el siguiente en la línea es el principal recurso de programación del siglo XXI: StackOverflow. O más bien, la respuesta, con un número de reacciones positivas superior a 1700 en el momento de escribir este artículo. La pregunta es: ¿Qué es el idioma de copiar e intercambiar? , y, como su nombre lo indica, no del todo sobre el tema que estamos considerando. Pero en su respuesta a esta pregunta, el autor también toca el tema que nos interesa. También aconseja usar el PPP "si el argumento se copia de todos modos" (es hora de introducir una abreviatura para esto, por Dios). Y en general, este consejo parece bastante apropiado, en el marco de su respuesta y el operator= discutido allí, pero el autor se toma la libertad de dar tal consejo de una manera más amplia, y no solo en este caso particular. Además, va más allá de todos los consejos que hemos cubierto hasta ahora y lo alienta a hacer esto incluso en código C++03. ¿Qué llevó al autor a tales conclusiones?

Aparentemente, el autor de la respuesta se inspiró principalmente en el artículo de otro autor de libros y desarrollador a tiempo parcial de Boost.MPL: Dave Abrahams. El artículo se llama “¿Quieres velocidad? Pasar por Valor. , y se publicó en agosto de 2009, es decir 2 años antes de la adopción de C++11 y la introducción de la semántica de movimiento. Como en casos anteriores, recomiendo que el lector se familiarice con el artículo, pero daré los principales argumentos (argumento, de hecho, uno) que da Dave a favor del PPP: necesita usar el PPP, porque el “ la optimización skip copy” funciona bien con él ( elisión de copia), que está ausente en PPSC. Si lees los comentarios del artículo, puedes ver que los consejos que promueve no son universales, lo que el propio autor confirma, respondiendo a las críticas de los comentaristas. Sin embargo, el artículo contiene un consejo explícito (guía) para usar el PPP si el argumento se va a copiar de todos modos. Por cierto, a quien le interese, puede leer el artículo “¿Quieres velocidad? No (siempre) pase por valor". . Como sugiere el título, este artículo es una respuesta al artículo de Dave, así que si ha leído el primero, ¡asegúrese de leer este también!

Desafortunadamente (afortunadamente para algunos), tales artículos y (especialmente) las respuestas populares en sitios populares dan lugar al uso masivo de técnicas dudosas (ejemplo banal) simplemente porque necesita escribir menos de esta manera, y el viejo dogma ya no es inquebrantable: siempre puedes referirte a "ese consejo popular de allá" si estás contra la pared. Ahora propongo familiarizarme con lo que nos ofrecen diversos recursos con recomendaciones para escribir código.

Porque varios estándares y recomendaciones ahora también se publican en la web, decidí referirme a esta sección a "sabiduría de la red". Entonces, aquí me gustaría hablar sobre dos fuentes cuyo propósito es hacer que los programadores de C++ codifiquen mejor proporcionando pautas sobre cómo escribir ese mismo código.

El primer conjunto de reglas que quiero considerar fue la última gota que me obligó a tomar este artículo después de todo. Este conjunto es parte de la utilidad clang-tidy y no existe fuera de ella. Como todo lo relacionado con clang, esta utilidad es muy popular y ya recibió integración con CLion y Resharper C++ (así fue como la encontré). Entonces, clang-tydy tiene una regla de modernización de paso por valor que funciona en constructores que toman argumentos a través de ECPS. Esta regla nos invita a reemplazar PPSK por PPZ. Además, en el momento de escribir este artículo, la descripción de esta regla contiene un comentario de que esta regla adiós funciona solo para constructores, pero ellos (¿quiénes son?) aceptarán con gusto la ayuda de aquellos que extenderán esta regla a otras entidades. Allí, en la descripción, también hay un enlace al artículo de Dave: está claro de dónde crecen las piernas.

Finalmente, como nota final sobre la sabiduría y las opiniones de otras personas, eche un vistazo a las pautas oficiales de codificación de C++: C++ Core Guidelines, cuyos principales editores son Herb Sutter y Bjarne Stroustrup (no está mal, ¿verdad?). Por lo tanto, estas recomendaciones contienen la siguiente regla: "Para los parámetros "in", pase los tipos de copia económica por valor y otros por referencia a const", lo que repite completamente la vieja sabiduría: PPSC está en todas partes y PPP es para objetos pequeños. Este consejo describe varias alternativas a considerar. en caso de que sea necesario optimizar el paso de argumentos. ¡Pero en la lista de alternativas, PPP no se presenta!

Dado que no tengo más fuentes dignas de atención, propongo proceder a un análisis directo de ambos métodos de transmisión.

Análisis

Todo el texto anterior está escrito de una manera un tanto inusual para mí: cito opiniones ajenas e incluso trato de no expresar la mía (sé que sale mal). En muchos sentidos, el hecho de que las opiniones de los demás, y mi objetivo era hacerlos breve reseña, pospuse una consideración detallada de ciertos argumentos que encontré de otros autores. En esta sección no me referiré a autoridades y daré opiniones, aquí consideraremos algunas de las ventajas y desventajas objetivas de PPSC y PPP, las cuales serán sazonadas con mi percepción subjetiva. Por supuesto, se repetirá algo de lo discutido anteriormente, pero, por desgracia, esta es la estructura de este artículo.

¿Hay una ventaja para PPP?

Entonces, antes de considerar los pros y los contras, propongo mirar qué y en qué casos nos da la ventaja de pasar por valor. Digamos que tenemos una clase como esta:

Class CopyMover ( public: void setByValuer(Contador porValor) ( m_ByValuer = std::move(byValuer); ) void setByRefer(const Accounter& byRefer) ( m_ByRefer = byRefer; ) void setByValuerAndNotMover(Contador byValuerAndNotMover) ( m_ByValuerAndNotMover = byValuerAndNotMover; ) void setRvaluer (Contador&& rvaluer) ( m_Rvaluer = std::move(rvaluer); ) );

Aunque para los propósitos de este artículo solo nos interesan las dos primeras funciones, he incluido cuatro opciones solo para usarlas como contraste.

La clase Accounter es una clase simple que cuenta cuántas veces se ha copiado/movido. Y en la clase CopyMover hemos implementado funciones que nos permiten considerar las siguientes opciones:

    Moviente el argumento pasado.

    Pase por valor, seguido de proceso de copiar el argumento pasado.

Ahora, si pasamos un lvalue a cada una de estas funciones así:

Contador por referencia; Contador por Valuador; Contador byValuerAndNotMover; copiarMover copiarMover; copyMover.setByRefer(byRefer); copyMover.setByValuer(byValor); copyMover.setByValuerAndNotMover(byValuerAndNotMover);

entonces obtenemos los siguientes resultados:

El claro ganador es el PPSC, ya que da solo una copia, mientras que PPZ da una copia y un movimiento.

Ahora intentemos pasar un rvalue:

copiarMover copiarMover; copyMover.setByRefer(Contador()); copyMover.setByValuer(Contador()); copyMover.setByValuerAndNotMover(Contador()); copyMover.setRvaluer(Contador());

Obtenemos lo siguiente:

No hay un ganador claro aquí, porque. tanto PPP como PPSC tienen una operación, pero debido al hecho de que PPP usa mover y PPSK usa copiar, puedes darle la victoria a PPP.

Pero nuestros experimentos no terminan ahí, agreguemos las siguientes funciones para simular una llamada indirecta (con el paso de argumento posterior):

void setByValuer(Contador byValuer, CopyMover& copyMover) ( copyMover.setByValuer(std::move(byValuer)); ) void setByRefer(const Accounter& byRefer, CopyMover& copyMover) ( copyMover.setByRefer(byRefer); ) ...

Los usaremos exactamente de la misma manera que lo hicimos sin ellos, por lo que no repetiré el código (busque en el repositorio si es necesario). Entonces, para un lvalue, los resultados serían:

Tenga en cuenta que el PPSK aumenta la brecha con el PPZ, y se va con una sola copia, mientras que el PPZ ya tiene la friolera de 3 operaciones (¡un movimiento más)!

Ahora pasamos un rvalue y obtenemos los siguientes resultados:

Ahora PPP tiene 2 movimientos, y PPSK sigue siendo la misma copia. ¿Es posible ahora nominar al PPP como ganador? No porque si un movimiento debe ser al menos tan bueno como una copia, ya no podemos decir lo mismo sobre 2 movimientos. Por lo tanto, no habrá ganador en este ejemplo.

Puede que me objeten: “Autor, usted tiene una opinión sesgada y atrae por los oídos lo que le conviene. ¡Incluso 2 movimientos serán más baratos que copiar! No puedo estar de acuerdo con esta afirmación. considerándolo todo, porque cuánto más rápido es moverse que copiar depende de la clase específica, pero veremos el movimiento "económico" en una sección separada.

Aquí tocamos algo interesante: agregamos una llamada indirecta y el PPP agregó "peso" a exactamente una operación. Creo que no es necesario tener un diploma de MSTU para entender que mientras más llamadas indirectas tengamos, más operaciones se realizarán utilizando el PPZ, mientras que para el PPSK el número permanecerá sin cambios.

Es poco probable que todo lo anterior sea una revelación para alguien, ni siquiera pudimos realizar experimentos; todos estos números deberían ser obvios para la mayoría de los programadores de C ++ de un vistazo. Es cierto que un punto aún merece aclaración: por qué, en el caso de rvalue, el PPP no tiene una copia (u otro movimiento), sino solo un movimiento.

Bueno, analizamos la diferencia de transferencia entre PPP y PPSK al ver de primera mano la cantidad de copias y transferencias. Aunque es obvio que la ventaja de PPP sobre PPSK incluso en ejemplos tan simples, por decirlo suavemente no Obviamente, todavía estoy, un poco a medias, llegando a la siguiente conclusión: si todavía copiamos el argumento de la función, entonces tiene sentido considerar pasar el argumento a la función por valor. ¿Por qué llegué a esta conclusión? Para pasar sin problemas a la siguiente sección.

Si copiamos...

Entonces, llegamos al notorio "si". La mayoría de los argumentos que encontramos no pedían la introducción generalizada del PPP en lugar del PPSP, solo pedían que se hiciera "si el argumento se copia de todos modos". Es hora de averiguar qué está mal con este argumento.

Quiero comenzar con una pequeña descripción de cómo escribo código. Últimamente, mi proceso de codificación se ha vuelto cada vez más similar a TDD. escribir cualquier método de clase comienza con escribir una prueba en la que aparece este método. En consecuencia, al comenzar a escribir una prueba y crear un método después de escribir la prueba, todavía no sé si copiaré el argumento. Por supuesto, no todas las funciones se crean de esta manera; a menudo, incluso en el proceso de escribir una prueba, sabe exactamente cuál será la implementación allí. ¡Pero este no es siempre el caso!

Alguien puede objetarme que no importa cómo se escribió originalmente el método, podemos cambiar la forma en que pasamos el argumento cuando el método se ha hecho realidad y está completamente claro para nosotros lo que está sucediendo allí (es decir, si tenemos o no una copia). ). Estoy parcialmente de acuerdo con esto; de hecho, puedes hacerlo de esa manera, pero nos involucra en una especie de juego extraño en el que tenemos que cambiar las interfaces solo porque la implementación ha cambiado. Lo que nos lleva al siguiente dilema.

Resulta que estamos modificando (o incluso planificando) la interfaz en función de cómo se implementará. No me considero un experto en programación orientada a objetos y otros cálculos teóricos de arquitectura de software, pero tales acciones contradicen claramente las reglas básicas cuando la implementación no debe afectar la interfaz. Por supuesto, ciertos detalles de implementación (ya sean específicos del idioma o de la plataforma de destino) aún se filtran a través de la interfaz de todos modos, pero debe intentar reducir en lugar de aumentar la cantidad de tales cosas.

Bueno, Dios lo bendiga, sigamos por este camino y sigamos cambiando las interfaces dependiendo de lo que hagamos en la implementación en términos de copiar el argumento. Digamos que escribimos el siguiente método:

Void setName(Nombre nombre) ( m_Name = mover(nombre); )

y envió nuestros cambios al repositorio. Con el paso del tiempo, nuestro producto de software adquirió nuevas funcionalidades, se integraron nuevos marcos y surgió la tarea de informar mundo exterior sobre los cambios en nuestra clase. Aquellos. se agregará algún mecanismo de notificación a nuestro método, que sea algo similar a las señales Qt:

Void setName(Nombre nombre) ( m_Name = mover(nombre); emit nameChanged(m_Name); )

¿Hay algún problema con este código? Hay. Para cada llamada a setName enviamos una señal, por lo que la señal se enviará incluso cuando sentido m_Name no ha cambiado. Además de los problemas de rendimiento, esta situación puede conducir a un bucle infinito debido al hecho de que el código que recibe la notificación anterior de alguna manera termina llamando a setName. Para evitar todos estos problemas, estos métodos suelen tener este aspecto:

Void setName(Nombre nombre) ( if(nombre == m_Name) return; m_Name = mover(nombre); emit nameChanged(m_Name); )

Nos deshicimos de los problemas anteriores, pero ahora nuestra regla "si copiamos de todos modos..." ha fallado: ya no hay una copia incondicional del argumento, ¡ahora solo lo copiamos si cambia! ¿Y qué hacemos ahora? ¿Cambiar interfaz? Bien, cambiemos la interfaz de clase debido a esta solución. Pero, ¿y si nuestra clase heredara este método de alguna interfaz abstracta? ¡Vamos a cambiarlo allí! ¿Hay muchos cambios debido al hecho de que la implementación ha cambiado?

Nuevamente, pueden objetarme, dicen, el autor, ¿por qué pensó en ahorrar en los partidos aquí, cuándo funcionará esta condición allí? ¡Sí, la mayoría de las llamadas serán falsas! ¿Hay alguna certeza en esto? ¿Dónde? Y si decidí ahorrar en partidos, ¿no fue el mismo hecho de que usáramos PPP el resultado de tales ahorros? Solo sigo la "línea del partido" por eficiencia.

Constructores

Repasemos brevemente los constructores, especialmente porque hay una regla especial para ellos en clang-tidy, que aún no funciona para otros métodos/funciones. Digamos que tenemos una clase como esta:

Class JustClass ( public: JustClass(const string& justString): m_JustString(justString) ( ) private: string m_JustString; );

Obviamente, el parámetro se copia y clang-tidy nos dirá que sería bueno reescribir el constructor de esta manera:

JustClass(cadena justString): m_JustString(mover(justString)) ( )

Y para ser honesto, es difícil para mí discutir aquí; después de todo, siempre copiamos la verdad. Y la mayoría de las veces, cuando pasamos algo a través de un constructor, lo copiamos. Pero más a menudo no significa siempre. Aquí hay otro ejemplo para ti:

Class TimeSpan ( public: TimeSpan(DateTime start, DateTime end) ( if(start > end) throw InvalidTimeSpan(); m_Start = move(start); m_End = move(end); ) private: DateTime m_Start; DateTime m_End; );

Aquí no siempre copiamos, sino solo cuando las fechas se presentan correctamente. Por supuesto, en la gran mayoría de los casos lo hará. Pero no siempre.

Se puede dar un ejemplo más, pero esta vez sin código. Imagina que tienes una clase que acepta un objeto grande. La clase existe desde hace mucho tiempo y ahora es el momento de actualizar su implementación. Nos damos cuenta de que de un objeto grande (que ha crecido con los años) no necesitamos más de la mitad, y tal vez incluso menos. ¿Podemos hacer algo al respecto pasando por valor? No, no podemos hacer nada, porque aún se creará la copia. Pero si usáramos PPSK, simplemente cambiaríamos lo que hacemos en el interior constructor. Y este es el punto clave: usando PPCS controlamos qué y cuándo sucede en la implementación de nuestra función (constructor), pero si usamos PPZ, entonces perdemos cualquier control sobre la copia.

¿Qué se puede aprender de esta sección? Que el argumento de "si copiamos de todos modos..." es muy discutible, porque lejos de siempre sabemos lo que vamos a copiar, e incluso cuando lo sabemos, muchas veces no estamos seguros de que esto continúe en el futuro.

Mudarse es barato

Desde el advenimiento de la semántica de movimiento, ha comenzado a tener un gran impacto en cómo se escribe el código C++ moderno y, con el tiempo, esta influencia solo se ha intensificado: no es sorprendente, porque el movimiento es tan barato en comparación con la copia. ¿Pero es? ¿Es verdad que el movimiento es siempre cirugia barata? Esto es lo que trataremos de tratar en esta sección.

gota

Comencemos con un ejemplo banal, digamos que tenemos una clase como esta:

Blob de estructura (std::array datos; );

Común gota(BDO, inglés BLOB), que se puede utilizar en una variedad de situaciones. Veamos lo que nos costará pasar por referencia y por valor. Nuestro BDO se usará así:

Almacenamiento vacío::setBlobByRef(const Blob& blob) ( m_Blob = blob; ) almacenamiento vacío::setBlobByVal(Blob blob) ( m_Blob = move(blob); )

Y llamaremos a estas funciones así:

Const Blob blob(); Almacenamiento de almacenamiento; almacenamiento.setBlobByRef(blob); almacenamiento.setBlobByVal(blob);

El código para otros ejemplos será idéntico a este, solo que con diferentes nombres y tipos, por lo que no lo daré para los ejemplos restantes: todo está en el repositorio.

Antes de pasar a las mediciones, intentemos predecir el resultado. Así que tenemos un std::array de 4 KB que queremos almacenar en un objeto de clase Storage . Como descubrimos anteriormente, para PPSC tendremos una copia, mientras que para PPP tendremos una copia y un movimiento. Basado en el hecho de que la matriz no se puede mover, habrá 2 copias para el PPP, contra una para el PPP. Aquellos. estamos justificados al esperar una doble superioridad en el desempeño de PPSC.

Ahora echemos un vistazo a los resultados de la prueba:

Esta y todas las pruebas posteriores se ejecutaron en la misma máquina con MSVS 2017 (15.7.2) y con el indicador /O2.

La práctica coincidió con la suposición: pasar por valor es 2 veces más costoso, porque para una matriz, mover es completamente equivalente a copiar.

Línea

Veamos otro ejemplo, un std::string normal. ¿Qué podemos esperar? Sabemos (cubrí esto en el artículo) que las implementaciones modernas distinguen entre dos tipos de cadena: corta (alrededor de 16 caracteres) y larga (las que son más cortas). Para los cortos, se usa un búfer interno, que es una matriz C regular de char , pero los largos ya estarán asignados en el montón. No nos interesan las líneas cortas, porque el resultado allí será el mismo que con BDO, por lo que nos centraremos en las colas largas.

Entonces, dada una cadena larga, es obvio que moverla debería ser bastante barato (solo mueva el puntero), por lo que puede esperar que mover la cadena no tenga ningún efecto en los resultados, y el PPP debería dar un resultado no peor que PPSK. Comprobemos en la práctica y obtengamos los siguientes resultados:

Pasemos a explicar este "fenómeno". Entonces, ¿qué sucede cuando copiamos una cadena existente a una cadena ya existente? Veamos un ejemplo banal:

Cadena primero (64, "C"); cadena segundo(64, "N"); //... segundo = primero;

Tenemos dos cadenas con un tamaño de 64 caracteres, por lo que al crearlas, el búfer interno no es suficiente, como resultado, ambas cadenas se colocan en el montón. Ahora copiamos primero a segundo. Porque tenemos los mismos tamaños de fila, obviamente segundo tiene suficiente espacio asignado para que quepan todos los datos de primero, entonces segundo = primero; sera un memcpy banal, nada mas. Pero si consideramos un ejemplo ligeramente modificado:

Cadena primero (64, "C"); cadena segundo = primero;

entonces ya no habrá una llamada a operator= , pero se llamará al constructor de copias. Porque estamos tratando con un constructor, entonces no hay memoria existente en él. Primero debe seleccionarse y luego copiarse primero. Aquellos. es la asignación de memoria y luego memcpy . Como usted y yo sabemos, la asignación de memoria en el almacenamiento dinámico global suele ser una operación costosa, por lo que copiar desde el segundo ejemplo será más costoso que copiar desde el primero. Más caro por asignación de almacenamiento dinámico.

¿Qué tiene que ver esto con nuestro tema? La más directa, porque el primer ejemplo muestra exactamente lo que sucede con PPSC, y el segundo muestra exactamente lo que sucede con PPP: siempre se crea una nueva línea para PPP, mientras que se reutiliza una existente para PPSK. Ya ha visto la diferencia en el tiempo de ejecución, por lo que no hay nada que agregar aquí.

Aquí nuevamente nos enfrentamos al hecho de que al usar el PPP, trabajamos fuera de contexto, por lo que no podemos disfrutar de todos los beneficios que puede proporcionar. Y si antes razonábamos en términos de cambios futuros teóricos, aquí estamos viendo una falla muy específica en el desempeño.

Por supuesto, se me puede objetar que, según dicen, la cuerda se destaca y la mayoría de los tipos no funcionan de esa manera. A lo que puedo responder lo siguiente: todo lo que se describió anteriormente será cierto para cualquier contenedor que asigne memoria en el montón inmediatamente para un montón de elementos. Además, ¿quién sabe qué otras optimizaciones sensibles al contexto se aplican en otros tipos?

¿Qué vale la pena aprender de esta sección? El hecho de que mover sea realmente barato no significa que reemplazar copiar con copiar+mover siempre producirá un rendimiento comparable.

tipo complejo

Finalmente, veamos un tipo que constará de múltiples objetos. Que sea una clase Persona, que consta de datos inherentes a cualquier persona. Por lo general, este es el nombre, apellido, código postal, etc. Puede pensar en todo esto como cadenas y asumir que las cadenas colocadas en los campos de la clase Person probablemente sean cortas. Aunque creo que en la vida real será la medida de las cuerdas cortas lo que será más útil, seguiremos considerando cuerdas de diferentes tamaños para que la imagen sea más completa.

También usaré Persona con 10 campos, pero para esto no crearé 10 campos directamente en el cuerpo de la clase. La implementación de Person esconde un contenedor en sus entrañas, es más conveniente cambiar los parámetros de la prueba de esta forma, prácticamente sin apartarse de cómo funcionaría si Person fuera una clase real. Sin embargo, la implementación está disponible y siempre puedes revisar el código y decirme si hice algo mal.

Así que vamos: una Persona con 10 campos de tipo string, que pasamos usando PPSC y PPZ a Storage:

Como puede ver, tenemos una gran diferencia en el rendimiento, lo que, después de las secciones anteriores, no debería sorprender a los lectores. También creo que la clase Person es lo suficientemente "real" como para no descartar tales resultados como abstractos.

Por cierto, cuando estaba preparando este artículo, preparé otro ejemplo: una clase que usa varios objetos std::function. Según mi idea, también tenía que mostrar una ventaja en el rendimiento de PPSK sobre PPZ, ¡pero resultó exactamente lo contrario! Pero no doy este ejemplo aquí, no porque no me gustaran los resultados, sino porque no tuve tiempo de averiguar por qué se obtienen tales resultados. Sin embargo, el código está en el repositorio (Impresoras), pruebas, también, si alguien desea comprender, me complacería conocer los resultados del estudio. Planeo volver a este ejemplo más adelante, y si nadie publica estos resultados antes que yo, los consideraré en un artículo separado.

Resultados

Por lo tanto, hemos analizado las diversas ventajas y desventajas de pasar por valor y por referencia a una constante. Observamos algunos ejemplos y observamos el rendimiento de ambos métodos en estos ejemplos. Por supuesto, este artículo no puede y no es exhaustivo, pero, en mi opinión, contiene suficiente información para tomar una decisión independiente e informada sobre qué método es mejor usar. Alguien puede objetar: "¿por qué usar un método, comencemos desde la tarea?". Aunque estoy de acuerdo con esta tesis en términos generales, no estoy de acuerdo con ella en esta situación. Creo que solo puede haber una forma de pasar argumentos en un idioma, cuál es el valor predeterminado.

¿Qué significa predeterminado? Esto significa que cuando escribo una función, no pienso en cómo debo pasar el argumento, solo uso el "predeterminado". El lenguaje C++ es un lenguaje bastante complejo que muchas personas evitan. Y en mi opinión, la complejidad no se debe tanto a la complejidad de las construcciones del lenguaje que están en el lenguaje (un programador típico puede que nunca las encuentre), sino al hecho de que el lenguaje te hace pensar mucho: ¿me liberé? hasta la memoria, no es caro usar esta función aquí, etc.

Muchos programadores (C, C++ y otros) desconfían y temen del C++ que comenzó a aparecer después de 2011. Escuché muchas críticas de que el lenguaje se está volviendo más complicado, que ahora solo los "gurús" pueden escribir en él, etc. Personalmente, creo que este no es el caso; por el contrario, el comité dedica mucho tiempo a hacer que el lenguaje sea más amigable para los principiantes y para que los programadores no tengan que pensar en las características del lenguaje. Después de todo, si no necesitamos luchar contra el idioma, entonces hay tiempo para pensar en el problema. Estas simplificaciones incluyen punteros inteligentes, funciones lambda y mucho más que ha aparecido en el lenguaje. Al mismo tiempo, no niego el hecho de que ahora se necesita estudiar más, pero ¿qué hay de malo en aprender? ¿O no hay cambios en otros idiomas populares que deban aprenderse?

Además, no tengo ninguna duda de que habrá snobs que puedan decir en respuesta: “¿Quieres pensar? Ve a escribir en PHP entonces”. Ni siquiera quiero responder a esta gente. Déjame darte solo un ejemplo de la realidad del juego: en la primera parte de Starcraft, cuando se crea un nuevo trabajador en un edificio, para que comience a extraer minerales (o gas), tenías que enviarlo manualmente allí. Además, cada paquete de minerales tenía un límite, más allá del cual la acumulación de trabajadores era inútil, e incluso podían interferir entre sí, empeorando la producción. En Starcraft 2, esto ha cambiado: los trabajadores automáticamente comienzan a extraer minerales (o gas), y también indica cuántos trabajadores están extrayendo actualmente y cuánto es el límite de este depósito. Esto simplificó enormemente la interacción del jugador con la base, permitiéndole concentrarse en más aspectos importantes juegos: construir una base, acumular tropas y destruir al enemigo. Parecería que esto es solo una gran innovación, ¡pero lo que comenzó en la red! La gente (¿quiénes son?) comenzó a gritar que el juego estaba "saliendo" y "mataron a Starcraft". Obviamente, tales mensajes solo podían provenir de "guardianes del conocimiento secreto" y "adeptos de alto APM" a quienes les gustaba estar en algún tipo de club de "élite".

Entonces, volviendo a nuestro tema, cuanto menos necesito pensar en cómo escribo el código, más tiempo tengo para pensar en resolver el problema inmediato. Pensar en qué método debo usar, PPSK o PPP, no me acerca ni un ápice a la solución del problema, así que simplemente me niego a pensar en esas cosas y elijo una opción: pasar por referencia a una constante. ¿Por qué? Porque no veo ninguna ventaja en el caso general de PPP, y los casos especiales deben considerarse por separado.

Un caso especial, es especial porque, habiendo notado que en algún método PPSC resulta ser un cuello de botella, y cambiando la transmisión a PPP, obtendremos un aumento importante en el rendimiento, no pienso usar PPP. Pero por defecto, usaré UCPS tanto en funciones normales como en constructores. Y si es posible, promoveré este método en particular siempre que sea posible. ¿Por qué? Porque creo que la práctica de promover el PPP es viciosa debido a que la mayor parte de los programadores no tienen muchos conocimientos (en principio o simplemente no están actualizados) y simplemente siguen los consejos. Además, si hay varios consejos en conflicto, entonces eligen el que es más simple, y esto lleva al hecho de que aparece pesimismo en el código simplemente porque alguien en algún lugar escuchó algo. Ah, sí, incluso que alguien pueda vincular el artículo de Abrahams para demostrar que tiene razón. Y luego te sientas, lees el código y piensas: pero el hecho de que el parámetro se pase por valor aquí se debe a que el programador que escribió esto vino de Java, solo leyó artículos "inteligentes", o ¿realmente hay una necesidad de PPZ?

PPSK se lee mucho más fácilmente: una persona conoce claramente el "buen tono" de C ++ y seguimos adelante: la apariencia no se detiene. La práctica de usar PPCS se ha enseñado a los programadores de C++ durante años, ¿cuál es la razón para rechazarla? Esto me lleva a otra conclusión: si la interfaz del método usa PPP, entonces también debería haber un comentario de por qué es así. En los demás casos, se aplicará el PPCS. Por supuesto, hay tipos de excepción, pero no los menciono aquí simplemente porque están implícitos: string_view, initializer_list, varios iteradores, etc. Pero estas son excepciones, cuya lista puede expandirse según los tipos que se usen en el proyecto. Pero el punto sigue siendo el mismo desde C++98: siempre usamos EPSC por defecto.

Para std::string , probablemente no habrá ninguna diferencia en cadenas pequeñas, hablaremos de eso más adelante.

Métodos de programación usando cadenas

Propósito del laboratorio : aprenda métodos en lenguaje C#, reglas para trabajar con datos de caracteres y con el componente ListBox. Escriba un programa para trabajar con cadenas.

Métodos

Un método es un elemento de una clase que contiene código de programa. El método tiene la siguiente estructura:

[atributos] [especificadores] nombre de tipo ([parámetros])

cuerpo del método;

Los atributos son indicaciones específicas para el compilador sobre las propiedades de un método. Rara vez se utilizan atributos.

Los especificadores son palabras clave para diferentes propósitos, por ejemplo:

· Determinar la accesibilidad del método para otras clases:

o privado- el método estará disponible solo dentro de esta clase

o protegido- el método también estará disponible para las clases de niños

o público- el método estará disponible para cualquier otra clase que pueda acceder a esta clase

Indicar la accesibilidad de un método sin crear una clase

Tipo de especificación

El tipo determina el resultado que devuelve el método: puede ser cualquier tipo disponible en C#, así como la palabra clave void si no se requiere ningún resultado.

El nombre del método es el identificador que se utilizará para llamar al método. Se aplican los mismos requisitos a un identificador que a los nombres de variables: puede constar de letras, dígitos y guiones bajos, pero no puede comenzar con un dígito.

Los parámetros son una lista de variables que se pueden pasar a un método cuando se llama. Cada parámetro consta de un tipo y un nombre de variable. Los parámetros están separados por una coma.

El cuerpo del método es un código de programa normal, excepto que no puede contener definiciones de otros métodos, clases, espacios de nombres, etc. Si el método debe devolver algún resultado, la palabra clave return debe estar presente al final con el valor devuelto. Si no desea devolver resultados, el uso de la palabra clave de retorno es opcional, aunque está permitido.

Un ejemplo de un método que evalúa una expresión:

public double Calc (doble a, doble b, doble c)

return Math.Sin(a) * Math.Cos(b);

doble k = Math.Tan(a * b);

volver k * Math.Exp(c / k);

Sobrecarga de métodos

El lenguaje C# le permite crear múltiples métodos con el mismo nombre pero diferentes parámetros. El compilador seleccionará automáticamente el método más apropiado al construir el programa. Por ejemplo, puede escribir dos métodos separados para elevar un número a una potencia: un algoritmo para números enteros y otro para números reales:

///

/// Calcular X elevado a Y para números enteros

///

Pow int privado (int X, int Y)

///

/// Calcular X elevado a Y para números reales

///

Pow doble privado (doble X, doble Y)

return Math.Exp(Y * Math.Log(Math.Abs(X)));

más si (Y == 0)

Dicho código se llama de la misma manera, la diferencia está solo en los parámetros: en el primer caso, el compilador llamará al método Pow con parámetros enteros, y en el segundo, con los reales:

Opciones predeterminadas

El lenguaje C# a partir de la versión 4.0 (Visual Studio 2010) le permite establecer valores predeterminados para algunos parámetros, de modo que al llamar a un método, puede omitir algunos de los parámetros. Para ello, al implementar el método, se debe asignar un valor a los parámetros requeridos directamente en la lista de parámetros:

privado void GetData(int Número, int Opcional = 5 )

Console.WriteLine("Número: (0)", Número);

Console.WriteLine("Opcional: (0)", Opcional);

En este caso, puede llamar al método de esta manera:

ObtenerDatos(10, 20);

En el primer caso, el parámetro Opcional será igual a 20, ya que está establecido explícitamente, y en el segundo, será igual a 5, porque explícitamente no está configurado y el compilador toma el valor predeterminado.

Los parámetros predeterminados solo se pueden establecer en el lado derecho de la lista de parámetros, por ejemplo, el compilador no aceptará una firma de método de este tipo:

privado void GetData (int Opcional = 5 , numeroint)

Cuando los parámetros se pasan a un método de la forma habitual (sin las palabras clave adicionales ref y out), cualquier cambio en los parámetros dentro del método no afecta su valor en el programa principal. Supongamos que tenemos el siguiente método:

privado void Calc(intNumber)

Se puede ver que dentro del método hay un cambio en la variable Número que se pasó como parámetro. Intentemos llamar al método:

Consola.WriteLine(n);

En pantalla aparecerá el número 1, es decir, a pesar del cambio de variable en el método Calc, el valor de la variable en el programa principal no ha cambiado. Esto se debe al hecho de que cuando se llama al método, Copiar variable pasada, eso es lo que modifica el método. Cuando el método termina, el valor de las copias se pierde. Esta forma de pasar un parámetro se llama pasando por valor.

Para que un método cambie la variable que se le pasa, debe pasarse con la palabra clave ref; debe estar tanto en la firma del método como cuando se llama:

privado void Calc (número de referencia int)

Consola.WriteLine(n);

En este caso, el número 10 aparecerá en la pantalla: cambiar el valor en el método también afectó al programa principal. Este método de transferencia se llama pasando por referencia, es decir. no se pasa una copia, sino una referencia a una variable real en memoria.

Si el método usa variables por referencia solo para devolver valores y no le importa lo que contenían originalmente, entonces no puede inicializar dichas variables, sino pasarlas con la palabra clave out. El compilador entiende que el valor inicial de la variable no es importante y no jura por la falta de inicialización:

privado void Calc (out int Número)

interno; // ¡No asigne nada!

tipo de datos de cadena

C# usa el tipo de cadena para almacenar cadenas. Para declarar (y, como regla, inicializar inmediatamente) una variable de cadena, puede escribir el siguiente código:

cadena a = "Texto";

cadena b = "cadenas";

Puede realizar una operación de suma en líneas; en este caso, el texto de una línea se agregará al texto de otra:

cadena c = a + " " + b; // Resultado: Línea de texto

El tipo de cadena es en realidad un alias para la clase String, que se puede utilizar para realizar una serie de operaciones más complejas en cadenas. Por ejemplo, el método IndexOf puede buscar una subcadena en una cadena y el método Substring devuelve una subcadena de una longitud específica, comenzando en una posición específica:

cadena a = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

índice int = a.IndexOf("OP"); // Resultado: 14 (cuenta desde 0)

cadena b = a.Subcadena(3, 5); // Resultado: DEFGH

Si necesita agregar caracteres especiales a una cadena, puede hacerlo usando secuencias de escape que comienzan con una barra invertida:

Componente ListBox

Componente cuadro de lista es una lista cuyos elementos se seleccionan con el teclado o el ratón. La lista de elementos viene dada por la propiedad Elementos. Items es un elemento que tiene sus propias propiedades y métodos. Métodos Agregar, Eliminar en y Insertar se utilizan para agregar, eliminar e insertar elementos.

Un objeto Elementos almacena los objetos en la lista. El objeto puede ser de cualquier clase: los datos de la clase se convierten para su visualización en una representación de cadena mediante el método ToString. En nuestro caso, las cadenas actuarán como un objeto. Sin embargo, dado que el objeto Items almacena objetos convertidos al tipo de objeto, debe volver a convertirlos a su tipo original, en nuestro caso, cadena, antes de usarlos:

cadena a = (cadena)listBox1.Items;

Para determinar el número del elemento seleccionado, utilice la propiedad ÍndiceSeleccionado.

Entonces, sea Factorial(n) la función para calcular el factorial de un número n . Entonces, dado que “sabemos” que el factorial 1 es 1, podemos construir la siguiente cadena:

Factorial(4)=Factorial(3)*4

Factorial(3)=Factorial(2)*3

Factorial(2)=Factorial(1)*2

Pero, si no tuviéramos una condición terminal de que cuando n = 1, la función Factorial debería devolver 1, entonces, en teoría, tal cadena nunca terminaría, y esto podría ser un error de desbordamiento de pila de llamadas: desbordamiento de pila de llamadas. Para comprender qué es la pila de llamadas y cómo puede desbordarse, veamos la implementación recursiva de nuestra función:

Función factorial(n: Entero): LongInt;

Si n=1 Entonces

Factorial:=Factorial(n-1)*n;

final;

Como podemos ver, para que la cadena funcione correctamente, antes de cada próxima llamada a la función en sí, es necesario guardar todas las variables locales en algún lugar, para que cuando se invierta la cadena, el resultado sea correcto (el valor calculado de el factorial de n-1 se multiplica por n). En nuestro caso, cada vez que se llama a la función factorial desde sí misma, se deben guardar todos los valores de la variable n. El área donde se almacenan las variables locales de una función cuando se llama recursivamente a sí misma se denomina pila de llamadas. Por supuesto, esta pila no es infinita y puede agotarse si las llamadas recursivas se construyen incorrectamente. La finitud de las iteraciones de nuestro ejemplo está garantizada por el hecho de que cuando n=1, la llamada a la función se detendrá.

Pasar parámetros por valor y por referencia

Hasta ahora, no podíamos cambiar el valor en la subrutina parámetro real(es decir, el parámetro que se especifica al llamar a la subrutina), y en algunas tareas de la aplicación esto sería conveniente. Recuerde el procedimiento Val , que cambia el valor de dos de sus parámetros reales a la vez: el primero es el parámetro donde se escribirá el valor convertido de la variable de cadena, y el segundo es el parámetro Código, donde el número del carácter erróneo se coloca, en caso de falla durante la conversión de tipo. Aquellos. todavía hay un mecanismo por el cual una subrutina puede cambiar los parámetros reales. Esto es posible debido a varias formas de pasar parámetros. Echemos un vistazo a estos métodos en detalle.

Programación Pascal

Pasar parámetros por valor

De hecho, así es como pasamos todos los parámetros a nuestras subrutinas. El mecanismo es el siguiente: al especificar el parámetro real, su valor se copia en el área de memoria donde se encuentra la subrutina y luego, después de que la función o el procedimiento haya completado su trabajo, esta área se borra. En términos generales, durante la ejecución de una subrutina, hay dos copias de sus parámetros: una en el ámbito del programa que llama y la segunda en el ámbito de la función.

Con este método de paso de parámetros, se tarda más en llamar a la subrutina, ya que además de la llamada en sí, es necesario copiar todos los valores de todos los parámetros reales. Si se pasa una gran cantidad de datos a la subrutina (por ejemplo, una matriz con una gran cantidad de elementos), el tiempo requerido para copiar los datos al área local puede ser significativo y esto debe tenerse en cuenta al desarrollar programas y encontrar cuellos de botella en su desempeño.

Con este método de transferencia, la subrutina no puede cambiar los parámetros reales, ya que los cambios afectarán solo el área local aislada, que se liberará después de que finalice la función o el procedimiento.

Pasar parámetros por referencia

Con este método, los valores de los parámetros reales no se copian en la subrutina, sino que se transfieren las direcciones en la memoria (referencias a variables) en las que se encuentran. En este caso, la subrutina ya está modificando valores que no están en el ámbito local, por lo que cualquier cambio también será visible para el programa que llama.

Para indicar que un argumento debe pasarse por referencia, se añade la palabra clave var antes de su declaración:

Procedimiento getTwoRandom(var n1, n2:Integer; range: Integer);

n1:=aleatorio(rango);

n2:=aleatorio(rango); final ;

var rand1, rand2: Entero;

Empezar obtenerDosAleatorio(al azar1, al azar2,10); WriteLn(rand1); WriteLn(rand2);

final.

En este ejemplo, las referencias a dos variables, rand1 y rand2, se pasan al procedimiento getTwoRandom como parámetros reales. El tercer parámetro real (10) se pasa por valor. El procedimiento se escribe mediante parámetros formales.